Mitch
06/04/2026, 12:06 AMMarvin
06/04/2026, 12:06 AMMarvin
06/04/2026, 12:09 AMpython
from prefect import flow
from prefect.deployments import run_deployment
def get_fresh_parameters(step: int, previous_result=None):
# Query DB / API / config service / whatever source of truth you use
return {
"step": step,
"previous_result": previous_result,
}
@flow
def orchestrate_chain():
previous_result = None
for i in range(10):
params = get_fresh_parameters(i, previous_result)
flow_run = run_deployment(
name=f"my-flow-{i}/prod",
parameters=params,
timeout=60 * 60 * 24 * 2, # wait up to 2 days, adjust as needed
)
# Depending on your pattern, inspect the state/result here
previous_result = flow_run.state
return "done"
A few notes:
- run_deployment(...) is the right primitive if these are separate deployments
- This gives you a true chain with one place to manage ordering
- The orchestrator itself can be very small — it doesn’t need to do the heavy work
If the flows are just code units and don’t need to be independently deployed, another option is to make them subflows and call them directly from one parent flow. Docs: Flows
Why not automations for this?
Automations can absolutely trigger flow B when flow A completes. Docs: Automations and Events
But they have drawbacks for your use case:
- parameter passing is much more limited than plain Python orchestration
- computing fresh parameters from external state just-in-time is awkward
- chaining 10 automations becomes harder to debug
- using outputs/results from the prior flow is not as natural
- strict sequencing is less explicit than “wait, then launch next”
So if your question is “do I really need another flow to manage these 10?” — for this case, yes, probably, and that’s actually the cleanest solution, not unnecessary overhead.
Rule of thumb
Use automations when:
- a run should happen because an event occurred
- flows are independent services
- you want reactive, event-driven orchestration
Use an orchestrator flow when:
- order matters
- next-step parameters depend on current runtime state
- you want simple logic, retries, branching, and observability
One more practical concern
Since each flow may run for ~1 day and create ~10k jobs, think about whether the “10 flows” are really:
- 10 stages in one pipeline, or
- 10 independent workloads that just need serialization
If they are pipeline stages, an orchestrator flow is definitely the natural model.
If they are independent but must not overlap, you could also consider work pool limits or concurrency controls in addition to orchestration, depending on why they must run one-at-a-time. Relevant docs:
- Work Pools
- Concurrency limitsMarvin
06/04/2026, 12:09 AMMitch
06/04/2026, 12:11 AMMarvin
06/04/2026, 12:11 AMMarvin
06/04/2026, 12:11 AMi, the orchestrator queries that source
- It launches flow i with the current parameters at that moment
- If a new model appears while flow 0 is running for a day, then when flow 1 is about to start, the orchestrator fetches again and sees the new model
That gives you exactly the “most up-to-date at invocation time” behavior.
Example:
python
from prefect import flow
from prefect.deployments import run_deployment
def get_latest_runtime_config(step: int) -> dict:
# Example: pull from database, API, S3, feature store, model registry, etc.
# This is called RIGHT BEFORE each downstream flow starts
return {
"model_version": fetch_latest_model_version(),
"step": step,
}
@flow
def orchestrate_chain():
previous_result = None
for i in range(10):
latest_params = get_latest_runtime_config(i)
if previous_result is not None:
latest_params["previous_result"] = previous_result
flow_run = run_deployment(
name=f"my-flow-{i}/prod",
parameters=latest_params,
timeout=60 * 60 * 48,
)
previous_result = flow_run.state
return "done"
So in your example:
- flow 0 starts with model v1
- 8 hours later, model v2 is released
- flow 0 finishes
- orchestrator is resumed
- orchestrator calls fetch_latest_model_version()
- flow 1 starts with v2
That is still fully compatible with an orchestrator flow.
Important distinction
There are two very different designs:
1. Push parameters at chain creation time
- stale for long-running chains
- not what you want
2. Resolve parameters at step launch time
- fresh
- what you want
You want #2.
When events/automations might help
If you want each flow to independently resolve its own runtime config, then automations can make sense only as the trigger, while the actual parameter freshness is handled inside the triggered flow itself.
For example:
- flow 0 completes
- automation triggers flow 1
- flow 1 starts with minimal input
- flow 1 itself queries the model registry / config store on startup
That can work well if:
- each flow is autonomous
- each flow knows how to fetch its own latest config
- you don’t need a central controller
In that design, you often pass only stable identifiers, like:
python
{
"customer_id": 123,
"pipeline_id": "abc"
}
and then inside the flow:
python
@flow
def flow_1(customer_id: int, pipeline_id: str):
runtime_config = fetch_latest_runtime_config(customer_id, pipeline_id)
...
So the real decision is this:
Option A — Orchestrator flow
- orchestrator decides when to launch next flow
- orchestrator fetches fresh config at launch time
Option B — Event-driven chain
- automation decides when to launch next flow
- downstream flow fetches fresh config on startup
For your case, I’d still lean toward A unless you specifically want a loosely coupled event-driven architecture.
My recommendation
If the only concern is “new model released mid-run”, then do one of these:
- Best overall: orchestrator fetches latest config right before each run_deployment
- Best decoupled design: each child flow fetches latest config on startup, and automation or orchestrator only passes stable identifiers
In practice, the most robust approach is often:
- orchestrator controls ordering
- child flows fetch their own latest model/config at start
That way:Marvin
06/04/2026, 12:11 AMMitch
06/04/2026, 12:16 AMMarvin
06/04/2026, 12:16 AMMarvin
06/04/2026, 12:16 AMpython
from prefect import flow
@flow
def step_1(pipeline_id: str):
config = fetch_latest_config(pipeline_id)
model = fetch_latest_model(config)
# do work
@flow
def step_2(pipeline_id: str):
config = fetch_latest_config(pipeline_id)
model = fetch_latest_model(config)
# do work
Then configure automations so:
- completion of step_0 triggers deployment of step_1
- completion of step_1 triggers deployment of step_2
- etc.
Docs:
- Automations
- Events
Option 2: External controller, not a Prefect flow
If you want centralized sequencing but don’t want a 2-week Prefect run, you can use:
- a small service
- a cron job
- a database-driven state machine
- or another scheduler
That controller can:
- watch for flow completion
- fetch latest config
- launch the next deployment via the API
This is good when sequencing logic is important but should live outside Prefect flow execution.
Option 3: Single flow / subflows only if this is really one pipeline stage graph
If these 10 “flows” are really just steps of one long pipeline, you could model them differently — but from what you described, they sound like independent long-running deployments, so I would not force them into one long parent run.
My actual recommendation for your use case
Given:
- 10 flows
- each can take ~1 day
- you want strict sequencing
- you want newest model/config at actual start time
I’d use:
- separate deployments for each step
- automations to trigger the next step on completion
- each flow fetches its own latest config/model at startup
That gives you:
- no 2-week parent run
- latest model picked up naturally
- clean separation between runs
One caveat
If you need very strict business logic like:
- “only start step 4 if step 3 finished and produced result X and external condition Y still holds”
- “skip step 6 if a newer model appeared”
- “restart from step 7 with revised parameters”
then automations alone can start getting awkward. In that case, an external stateful controller is often better than either:
- a 2-week orchestrator flow, or
- a large automation chain
So the practical choice is:
- Simple sequential chain + fresh startup config → use automations
- Complex multi-day state machine → use an external controller/service
If you want, I can show you how to model the automation chain in Prefect 3 for flow_0 -> flow_1 -> ... -> flow_9, with each flow resolving the latest model at startup.Mitch
06/04/2026, 12:18 AMMarvin
06/04/2026, 12:18 AMMarvin
06/04/2026, 12:18 AMpython
@flow
def train_step(pipeline_id: str):
config = load_current_pipeline_config(pipeline_id)
model_info = load_latest_model_info(config)
...
Then the automation for flow_1 is basically:
- when deployment/run for flow_0 completes successfully
- run deployment flow_1
- pass pipeline_id
That means if a new model is released while flow_0 is running, flow_1 will pick it up when it starts.
So to answer directly:
Yes — the automation-based sequential chaining you proposed initially is likely the best architecture for this use case.
I’d summarize it as:
- Use automations for ordering
- Use external state/config stores for freshness
- Do not use a long-lived parent flow
- Do not try to make automations carry all dynamic state
If you want, I can give you a concrete Prefect 3 example of:
- 2 chained deployments with an automation trigger, and
- the downstream flow fetching fresh config at runtime
and then you can replicate it for all 10.Mitch
06/04/2026, 12:21 AMMarvin
06/04/2026, 12:21 AMMarvin
06/04/2026, 12:21 AMbucket
- pipeline_id
- tenant_id
- region
2. Dynamic parameters
Better resolved at flow start:
- model_version
- config_revision
- latest_checkpoint
- active_ruleset
A good pattern is:
python
from prefect import flow
@flow
def step_1(bucket: str, pipeline_id: str):
runtime_config = fetch_latest_config(pipeline_id)
model_version = runtime_config["model_version"]
# use bucket + latest model_version
Then the automation that triggers step_2 passes along the same stable identifiers.
Conceptually:
- step_0(bucket="my-bucket", pipeline_id="abc")
- completion triggers step_1(bucket="my-bucket", pipeline_id="abc")
- completion triggers step_2(bucket="my-bucket", pipeline_id="abc")
while each step independently does:
python
config = fetch_latest_config(pipeline_id)
How to pass stable parameters to the next deployment
In Prefect automations, the RunDeployment action can provide parameters to the triggered deployment. For a chain, you’d configure each downstream deployment with the stable values it needs.
If the values are constant for that chain, common options are:
Option A: bake them into the deployment
If bucket is always the same for that deployment/environment, just make it a default deployment parameter.
Option B: pass them explicitly in the automation
If bucket varies by chain/run, set it in the automation-triggered deployment parameters.
Option C: store chain context externally
If you have many values to carry across 10 steps, store a record keyed by pipeline_id, then each flow reads from it.
For long multi-step chains, Option C is often the cleanest.
For example:
python
@flow
def step_3(pipeline_id: str):
context = load_pipeline_context(pipeline_id)
bucket = context["bucket"]
latest_model = context["latest_model"]
That avoids having to thread many params through every automation.
What I’d recommend
If it’s just a few stable values like bucket + pipeline ID:
- pass them as normal parameters
If it’s a lot of shared context:
- pass only pipeline_id
- store everything else in DB/object storage/config store
- let each flow load what it needs
Best practical design for your example
Pass this kind of thing as parameters:
python
{
"pipeline_id": "abc123",
"bucket": "my-bucket"
}
Then inside every flow:
python
context = fetch_context(pipeline_id)
latest_model = context["latest_model"]
That gives you:
- stable bucket available everywhere
- dynamic model always fresh
If you want, I can show you a Prefect 3 automation example where flow_0 completion triggers flow_1 and passes bucket and pipeline_id into the next run.Mitch
06/04/2026, 12:23 AMMarvin
06/04/2026, 12:23 AMMarvin
06/04/2026, 12:25 AMRunDeployment action’s parameters field, using Jinja templating to read values from the upstream flow_run.
So for stable params like bucket or pipeline_id, you can configure the downstream automation to copy them from the completed upstream run.
Example shape:
python
from prefect.automations import Automation, EventTrigger, RunDeployment, Posture
from prefect.events.schemas.events import ResourceSpecification
automation = Automation(
name="flow-0 -> flow-1",
trigger=EventTrigger(
expect={"prefect.flow-run.Completed"},
match_related=ResourceSpecification({
"prefect.resource.role": "flow",
"prefect.resource.name": "flow-0",
}),
posture=Posture.Reactive,
threshold=1,
),
actions=[
RunDeployment(
source="selected",
deployment_id="<flow-1-deployment-id>",
parameters={
"bucket": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['bucket'] }}",
},
"pipeline_id": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['pipeline_id'] }}",
},
},
)
],
)
That means:
- flow-0 is started with bucket="my-bucket" and pipeline_id="abc"
- when flow-0 completes
- the automation triggers flow-1
- flow-1 gets those same values by reading them from `flow-0`’s parameters
So yes, this is how you thread stable parameters through a chain.
Important caveat about types
Plain Jinja rendering can turn values into strings. If you need to preserve structured types, use the JSON hydration form.
For strings like bucket names, this is fine:
python
"bucket": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['bucket'] }}",
}
For non-strings, use:
python
"some_dict": {
"__prefect_kind": "json",
"value": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['some_dict'] | tojson }}",
},
}
What you can access in automation templates
The upstream run is available as flow_run, so common examples are:
python
{{ flow_run.parameters['bucket'] }}
{{ flow_run.parameters['pipeline_id'] }}
{{ flow_run.id }}
You can also mix literal values with copied values:
python
parameters={
"bucket": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['bucket'] }}",
},
"pipeline_id": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['pipeline_id'] }}",
},
"environment": "prod",
}
How this fits your use case
For your chain, a good split is:
- pass bucket, pipeline_id, maybe tenant_id through automations
- do not pass highly dynamic things like latest_model_version unless you really want to freeze them at trigger time
- instead, let the downstream flow resolve those fresh at startup
So the chain becomes:
- start flow_0(bucket, pipeline_id)
- automation triggers flow_1(bucket, pipeline_id)
- automation triggers flow_2(bucket, pipeline_id)
- etc.
with bucket copied via:
python
{{ flow_run.parameters['bucket'] }}
Docs / concepts
- Automations
- Events
If you want, I can give you a full 2-step Prefect 3 example showing:
- two deployments
- the exact automation definition
- and how bucket / pipeline_id get passed from flow_0 to flow_1.Mitch
06/04/2026, 12:27 AMMarvin
06/04/2026, 12:27 AMMarvin
06/04/2026, 12:27 AMRunDeployment action.
The pattern is:
1. your lambda / cron detects a new dataset
2. it starts flow 0 with parameters like:
- dataset_hash
- maybe bucket
- maybe pipeline_id
3. when flow 0 completes, an automation triggers flow 1
4. the automation copies dataset_hash from `flow 0`’s parameters into flow 1
5. repeat for all downstream steps
So the hash becomes part of the chain context.
What starts the chain?
Your external poller would create the first flow run / deployment run for step 0.
For example, conceptually:
python
parameters = {
"dataset_hash": discovered_hash,
"bucket": "incoming-datasets",
"pipeline_id": "dataset-processing",
}
Then every downstream automation copies those values from the triggering flow_run.parameters.
How to pass it through automations
In each RunDeployment action:
python
parameters={
"dataset_hash": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['dataset_hash'] }}",
},
"bucket": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['bucket'] }}",
},
"pipeline_id": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['pipeline_id'] }}",
},
}
That tells Prefect:
- read these values from the completed upstream flow run
- pass them into the next deployment
So if flow_0 started with:
python
{
"dataset_hash": "abc123",
"bucket": "incoming-datasets",
"pipeline_id": "dataset-processing"
}
then flow_1 receives the same values, and then flow_2, etc., as long as each automation copies them forward.
Best design
For your case, I’d treat dataset_hash as a chain identifier.
That means every step gets:
- dataset_hash
- other stable identifiers
and then each step can use that hash to:
- query the DB for latest metadata
- find the uploaded object
- resolve current model/config
- write step status back to the DB
That is often cleaner than trying to pass a lot of context through each automation.
So maybe every flow receives only:
python
{
"dataset_hash": "...",
}
and then does:
python
record = load_dataset_record(dataset_hash)
bucket = record["bucket"]
latest_model = record["latest_model"]
...
That’s often the most robust design for long chains.
Two good options
Option A: pass the hash and a few stable params through every automation
Good if the context is small.
Option B: pass only the hash through every automation
Best if the hash can be used to look up everything else.
For most real systems, Option B is cleaner.
Very important limitation
Automations don’t maintain some built-in “workflow memory”. They only:
- react to events
- launch the next deployment
- pass parameters you explicitly configure
So if you want dataset_hash at every step, you must either:
- explicitly pass it in every automation, or
- pass a single key like dataset_hash and have each step look up the rest externally
Recommended architecture for your case
- Lambda/cron detects new dataset
- Lambda launches step_0 deployment with dataset_hash
- Automation step_0 completed -> step_1 copies dataset_hash
- Automation step_1 completed -> step_2 copies dataset_hash
- ...
- each step uses dataset_hash to fetch the current record/config/state
That gives you:
- no long-lived orchestrator
- a stable chain key
- fresh runtime lookups at each step
If you want, I can show you:
1. how the lambda should start the first deployment, and
2. the exact automation config for copying dataset_hash to the next step.Mitch
06/04/2026, 12:28 AMMarvin
06/04/2026, 12:28 AMMarvin
06/04/2026, 12:30 AMabc123, trigger the first deployment with that hash as a parameter.
Using the CLI, the command exists as:
bash
prefect deployment run <FLOW_NAME>/<DEPLOYMENT_NAME> --param key=value
Verified help: prefect deployment run --help
Example:
bash
prefect deployment run flow-0/prod \
--param dataset_hash="abc123" \
--param bucket="incoming-datasets" \
--param pipeline_id="dataset-processing"
You can do the same from Python:
python
from prefect.deployments import run_deployment
run_deployment(
name="flow-0/prod",
parameters={
"dataset_hash": "abc123",
"bucket": "incoming-datasets",
"pipeline_id": "dataset-processing",
},
timeout=0, # return immediately
)
So your Lambda / cron is only responsible for starting step 0.
2. Pass those same parameters through the automation to step 1
This is the part you were asking about.
In the automation’s RunDeployment action, set parameters with Jinja templates that read from the upstream flow_run.parameters.
Example:
python
from datetime import timedelta
from prefect.automations import Automation, EventTrigger, RunDeployment, Posture
from prefect.events.schemas.events import ResourceSpecification
automation = Automation(
name="flow-0 -> flow-1",
trigger=EventTrigger(
expect={"prefect.flow-run.Completed"},
match_related=ResourceSpecification({
"prefect.resource.role": "deployment",
"prefect.resource.name": "flow-0",
}),
posture=Posture.Reactive,
threshold=1,
within=timedelta(seconds=0),
),
actions=[
RunDeployment(
source="selected",
deployment_id="<flow-1-deployment-id>",
parameters={
"dataset_hash": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['dataset_hash'] }}",
},
"bucket": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['bucket'] }}",
},
"pipeline_id": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['pipeline_id'] }}",
},
},
)
],
)
Then for flow-1 -> flow-2, do the same thing again:
python
parameters={
"dataset_hash": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['dataset_hash'] }}",
},
"bucket": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['bucket'] }}",
},
"pipeline_id": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['pipeline_id'] }}",
},
}
So the values are copied forward from run to run.
3. Recommended design
For your use case, I would usually pass:
- dataset_hash
- bucket
- pipeline_id
through every automation, and let each flow use dataset_hash to look up fresh metadata if needed.
That gives you:
- stable chain context passed forward via automation
- fresh dynamic config loaded by each step at runtime
4. Important caveat: preserve types if needed
For strings like dataset_hash and bucket, the simple Jinja form is fine.
If you ever need to pass a dict/list/int and preserve the type, use the JSON wrapper:
python
"payload": {
"__prefect_kind": "json",
"value": {
"__prefect_kind": "jinja",
"template": "{{ flow_run.parameters['payload'] | tojson }}",
},
}
5. Alternative: start the chain from an event instead of directly running flow 0
If you want, your Lambda could emit a custom event instead, and an automation could start flow-0 using values from event.payload.
That would look like:
```python
parameters={
"dataset_hash": {Marvin
06/04/2026, 12:30 AMflow_run.parameters[...] to keep forwarding them.
That approach is nice if you want the whole thing to be fully event-driven.
Summary
Your end-to-end flow is:
1. Lambda/cron detects new dataset
2. it starts flow-0 with:
- dataset_hash
- bucket
- pipeline_id
3. automation for flow-0 completed starts flow-1 and copies those params
4. automation for flow-1 completed starts flow-2 and copies those params
5. repeat through all steps
Useful docs:
- Automations
- Events
If you want, I can give you a full minimal working example with:
- flow_0
- flow_1
- one automation
- and the exact Lambda snippet to launch flow_0.