Quickstart
This page walks through a minimal pipeline — a single training task — to introduce the full pattern without getting distracted by multi-step complexity. For a pipeline with multiple tasks and more config structure, see Full Example: Training Pipeline.
import logging
import hydra
from flytekit import Resources, workflow
from flytekitplugins.omegaconf import OmegaConfTransformerMode, set_transformer_mode
from hydra.conf import HydraConf
from hydra.types import RunMode
from hydra_zen import builds, make_config, ZenStore
from launcher_conf import launcher_store
from omegaconf import DictConfig, MISSING
from scaffold.flyte import run_local_workflow, runtime_task, zen_call
logger = logging.getLogger(__name__)
set_transformer_mode(mode=OmegaConfTransformerMode.DictConfig)
workflow_store = ZenStore(name="workflow")
# ── 1. Configure Logic ────────────────────────────────────────────────────────
class Model:
def __init__(self, name: str, learning_rate: float = 1e-3):
self.name = name
self.learning_rate = learning_rate
ModelConf = builds(Model, name=MISSING, populate_full_signature=True)
model_store = workflow_store(group="model")
model_store(ModelConf(name="resnet18", learning_rate=1e-3), name="resnet18")
model_store(ModelConf(name="resnet50", learning_rate=5e-4), name="resnet50")
# ── 2. Implement Logic ────────────────────────────────────────────────────────
def train(model: Model, data_path: str, output_path: str) -> str:
logger.info(f"Training {model.name} on {data_path}, saving to {output_path}")
return output_path
# ── 3. Configure Task and Workflow ────────────────────────────────────────────
TrainConf = builds(train, model=MISSING, data_path=MISSING, output_path=MISSING, populate_full_signature=True)
WorkflowConf = make_config(
hydra_defaults=[
"_self_",
{"model@train.model": "resnet18"},
{"override hydra/launcher": "flyte"},
],
train=TrainConf,
)
workflow_store(
make_config(
bases=(WorkflowConf,),
train=TrainConf(data_path="gs://example/data", output_path="gs://example/models"),
),
name="default",
)
# ── 4. Implement Task and Workflow ────────────────────────────────────────────
@runtime_task(requests=Resources(mem="4Gi", cpu="2"))
def train_task(cfg: DictConfig, runtime_cfg: DictConfig) -> str:
return zen_call(train, cfg)
@workflow
def training_pipeline(train: DictConfig, runtime_cfg: DictConfig) -> str:
return train_task(cfg=train, runtime_cfg=runtime_cfg)
# ── 5. Run ────────────────────────────────────────────────────────────────────
@hydra.main(config_name="default", config_path=None, version_base="1.3")
def main(cfg: DictConfig) -> None:
run_local_workflow(training_pipeline, cfg)
if __name__ == "__main__":
workflow_store(HydraConf(mode=RunMode.MULTIRUN))
workflow_store.add_to_hydra_store(overwrite_ok=True)
launcher_store.add_to_hydra_store(overwrite_ok=True)
main()
Running the pipeline
Locally:
python workflow.py hydra.launcher.execution_environment=local
This executes main() directly, which calls run_local_workflow. Flyte tasks and
the workflow run as ordinary Python functions in the current process — no containers, no Flyte backend.
Remotely:
See the Deployment Configuration section for instructions on setting up your Flyte environment and configuring the launcher.
Overriding config from the CLI
Because the config is registered with Hydra, all standard Hydra overrides work:
# Use resnet50 instead of the default resnet18
python workflow.py train.model=resnet50
# Override a nested field
python workflow.py train.model.learning_rate=1e-4
# Show the rendered config without running
python workflow.py --cfg job
Pattern walkthrough
Step 1 — Configure Logic
builds() creates a structured config dataclass from a regular class. With
populate_full_signature=True the config inherits the full signature including type hints,
which Hydra uses for validation. MISSING marks required fields that have no default.
Multiple named variants are registered in a config group using a sub-store:
model_store = workflow_store(group="model")
model_store(ModelConf(name="resnet18"), name="resnet18")
model_store(ModelConf(name="resnet50"), name="resnet50")
The hydra_defaults list in WorkflowConf selects resnet18 by default and
places it at train.model in the rendered config.
Step 2 — Implement Logic
Plain Python. No Flyte, no hydra, no Scaffold. This is intentional — functions are fully testable
in isolation and zen_call handles the bridge to the config system.
Step 3 — Configure Task and Workflow
builds(train, ...) creates a config for the task. The named config
workflow_store(..., name="default") provides the concrete values (paths, etc.) that fill
in the MISSING fields.
Step 4 — Implement Task and Workflow
@runtime_task is a drop-in for @task that additionally:
Calls
apply_runtime_cfg(runtime_cfg)at the start of every remote execution to restore logging and other runtime settings inside the containerAutomatically excludes
runtime_cfgfrom Flyte’s cache key, so changing log verbosity never invalidates a cached task result
zen_call(fn, cfg) instantiates all config fields (calling instantiate() on any
builds() sub-config) and passes the results as keyword arguments to fn.
In this example, cfg.model is a ModelConf — zen_call turns it into a Model instance.
The @workflow receives one DictConfig per task plus runtime_cfg, all
resolved from the top-level config by run_local_workflow (locally) or by the launcher (remotely).
Step 5 — Run
run_local_workflow maps config keys to workflow inputs by name and executes the workflow.
The __main__ block adds the HydraConf(mode=RunMode.MULTIRUN) entry to the store
and flushes both stores before calling main().
Note
The launcher_conf.py file is imported at the top of the workflow file. It defines the
project-specific Docker image configuration and is kept separate so it can be shared across
multiple workflow files. See Deployment Configuration for its contents and all available options.