Durable vs transient workflows

Choose per workflow whether Flux persists every task-level checkpoint or only the outer lifecycle — and when at-most-once transient execution is the right trade.

Every Flux workflow declares a durability mode:

@workflow.with_options(durability="durable")     # the default
async def billing_run(ctx: ExecutionContext[str]): ...

@workflow.with_options(durability="transient")
async def agent_hop(ctx: ExecutionContext[str]): ...

durable is the mode described everywhere else in these docs: every task-level state transition is persisted as an event, so a crashed execution replays from the last checkpoint and completed tasks never re-run. transient deliberately gives that up for workflows where replay is worthless and event volume is the dominant cost.

What durable buys you

With durability="durable" (the default, and what you get from a bare @workflow):

If you are unsure, stay durable. See Why durable execution for the argument from first principles.

What transient changes

With durability="transient", only the outer lifecycle is persisted: the execution row, dispatch, and terminal state. The execution still appears in flux execution list with its final COMPLETED/FAILED state, but the worker suppresses every intermediate checkpoint and the terminal checkpoint carries only the WORKFLOW_* events.

The exact semantics and restrictions:

When transient is right

Transient exists for workflows where the intermediate history has no value but its storage cost is real:

Pair with runner="inprocess" to also skip the per-execution process spawn — the lowest-latency configuration for trusted mesh hops:

@workflow.with_options(durability="transient", runner="inprocess")
async def classify(ctx: ExecutionContext[str]): ...

The same-worker fast path

A mode="sync" call() whose target is a transient workflow object — the decorated object itself, not a string reference — with runner unset or "inprocess" executes in-process on the calling worker: no dispatch round-trip, no execution row, no checkpoints. Measured: ~2.3 ms median per hop versus ~526 ms for a server-relayed transient execution. This is the true agent-to-agent path.

from flux.tasks import call

@workflow.with_options(durability="transient")
async def summarize(ctx: ExecutionContext[str]): ...

@workflow
async def agent(ctx: ExecutionContext[str]):
    # Object target + transient + sync => same-worker fast path
    result = await call(summarize, ctx.input)

What to know about a fast-path hop:

Disable the fast path fleet-wide with [flux.workers] transient_fast_path = false (or FLUX_WORKERS__TRANSIENT_FAST_PATH=false) to force every call() through the server.

Quick comparison

durable (default)transient
Task-level checkpointsEvery transition persistedNone
Crash recoveryRe-dispatch + replay from last checkpointTerminal failure; caller retries
Retry semanticsCompleted tasks never re-runAll tasks re-run from scratch
Pause / approvalsSupportedTransientDurabilityError
SchedulesSupportedRejected at decoration time
Visible in flux execution listYesYes (outer lifecycle only)
Event rows (8-task workflow, measured)~19.9~4.0

What’s next