Skip to main content

Flow options

Dex allows you to customize the behavior of a Flow execution.

  • StartFlowOptions apply when a client starts a Flow (or SubFlow). They are immutable after start.
    • They control timeout, ID reuse, initial Attributes, request idempotency, and optional config overrides for that execution.
  • FlowConfig provide runtime behavior. They are mutable after start.
    • Step durability defaults, Worker routing, active-Step search indexing, and Attribute Store projection targets.

StartFlowOptions​

FieldPurpose
TimeoutFlow timeout. Omit it or set zero to disable it.
TimeoutPolicyWhat happens when the flow timeout fires.
TimeoutHandlerOptionsExecution, retry, recovery, durability, locking, and state loading for a timeout handler.
IDReusePolicyHow to reuse existing Flow ID to start
RequestIDIdempotency key for retrying one start request.
AlreadyStartedOptionsHow to handle an already-started Flow.
RetryPolicyNot supported. server-zd rejects it with INVALID_ARGUMENT. See RetryPolicy.
StartDelayNot supported. server-zd rejects a positive value with INVALID_ARGUMENT. See StartDelay.
AttributesInitial Attribute values.
FlowConfigPer-execution Flow configuration

For an application-defined outcome at the Flow deadline, see Graceful Timeout.

def example_start_flow_options() -> StartFlowOptions:
return StartFlowOptions(
timeout=timedelta(minutes=30),
timeout_policy=FlowTimeoutPolicy.HANDLER,
id_reuse_policy=IdReusePolicy.DISALLOW,
config_override=FlowConfig(step_durability=StepDurability.SYNC),
ignore_already_started=True,
request_id="start-order-123",
).with_attribute(status, "queued")

Example: examples/python/dex_examples/primitives/flow/controller.py

Timeout and TimeoutPolicy​

Timeout is one durable timer for the whole Flow. Set TimeoutPolicy only when Timeout is positive.

  • FAIL — fail the Flow with a timeout error.
  • CANCEL — cancel the Flow. It does not retry.
  • HANDLER — run one logical Flow timeout-handler execution.

With a positive Timeout, a Dex SDK uses FAIL by default. If the Flow has a timeout handler, it uses HANDLER instead. HANDLER is invalid when the Flow has no handler.

TimeoutHandlerOptions is valid only with a positive Timeout and a final HANDLER policy. It uses Execute semantics: a method timeout limits each attempt, Retry can run multiple attempts, HeartbeatTimeout detects stalled attempts, and Failure can route exhausted retries to a registered no-input Step. The recovery Step receives null or unit input and reads the final error from the handler Context.

The handler's attempt and retry clocks begin when the soft Flow timeout fires. They may extend past the original Flow deadline. If no failure target is configured, an exhausted handler fails the Flow.

Ordinary Attributes and all Channel size metadata load automatically. AttributeMap values and pending Channel messages require explicit selections in TimeoutHandlerOptions. The handler may stage Attribute writes, Channel deletions, and Channel publications before returning its decision. Flow-level retry does not exist, so the timeout handler runs once per Flow run.

Set TimeoutPolicy to HANDLER to run this code when the timeout fires. This handler records why the Flow ended, then fails it with an application reason.

def handle_timeout(self, context: Context) -> StepDecision:
status.set(context, "timed out")
return force_fail("processing deadline reached")

Example: examples/python/dex_examples/primitives/flow/example_flow.py

Flow ID, RequestID, and AlreadyStartedOptions​

IDReusePolicy says what to do when this Flow ID already exists. RequestID says whether this call retries the same start request.

How these options work together​

These options answer different questions, in this order:

  1. RequestID identifies one start request. When omitted, the SDK generates a new UUID, so another StartFlow call is a new request. Reuse it only for a retry of the exact same start. Dex compares the RequestID, not the start input or options.
  2. IDReusePolicy decides whether that request may start another run with this Flow ID.
  3. AlreadyStartedOptions.IgnoreError matters only when the ID reuse policy rejects the start as already started. It returns the existing Run ID only when the RequestID matches.
Retry requestAlreadyStartedOptions.IgnoreErrorWhen the ID reuse policy rejects the start
No RequestIDOff or onThe SDK sends a new UUID. Dex returns an already-started error.
A different RequestIDOff or onDex returns an already-started error.
Same RequestIDOffDex returns an already-started error.
Same RequestIDOnDex returns the existing Run ID.

The ID reuse policy determines whether Dex rejects the start in the first place:

IDReusePolicyExisting active FlowExisting closed Flow
IDReuseDefaultRejects the start. A matching RequestID with IgnoreError returns the active Run ID.Rejects the start. A matching RequestID with IgnoreError returns the existing Run ID.
IDReuseAllowIfPreviousFailedRejects the start. A matching RequestID with IgnoreError returns the active Run ID.Starts a new run only after a failed run; otherwise rejects it.
IDReuseAllowIfNotRunningRejects the start. A matching RequestID with IgnoreError returns the active Run ID.Starts a new run.
IDReuseDisallowRejects the start. A matching RequestID with IgnoreError returns the existing Run ID.Rejects the start. A matching RequestID with IgnoreError returns the existing Run ID.
IDReuseTerminateIfRunningTerminates the active run and starts a replacement. It does not produce an already-started error.Starts a new run.

What Dex Server does​

Dex Server requires a non-empty RequestID. An SDK generates one before sending the request when you omit it. The server records it with the Flow execution, then asks the configured backend to start the Flow using the requested ID reuse policy. IDReuseDefault uses the server default: reject every previously used Flow ID. Choose IDReuseAllowIfNotRunning explicitly when a closed Flow ID should start a new execution.

If the backend reports that the Flow is already started, Dex checks AlreadyStartedOptions.IgnoreError. When it is on, Dex reads the existing run's recorded RequestID. It returns that Run ID only when the two RequestIDs match. Otherwise it returns the already-started error.

StartFlow best practices​

Call StartFlow directly with a stable business Flow ID. Do not read a snapshot, search for the Flow, or check its status first just to avoid a possible retry conflict. A successful start also needs no read to recheck the identity, owner, or initial Attributes you supplied.

For retries of one logical start, combine a stable RequestID, IDReuseDisallow, and the ignore-already-started option. Keep the same input and options on every retry. IDReuseDisallow prevents another execution after closure; a policy that permits another run can bypass the matching-RequestID path.

The following helpers accept a validated integer input and a stable Flow ID. Those are the complete business inputs in this example, so together they determine the RequestID. All other start values are fixed. In a real application, include every logical input that changes the operation, such as tenant, owner, mode, revision, or initial business Attributes. Dex compares RequestIDs; it does not compare payload equality. Do not reuse a RequestID for a changed logical request.

Each helper returns the known business Flow ID after acceptance and propagates errors. It performs no read before or after the start. The returned ID is an acknowledgement, not a current business snapshot or proof that the Flow has completed.

async def start_example_flow_request(
client: AsyncClient, example_flow: ExampleFlow, flow_id: str, input_num: int
) -> str:
options = StartFlowOptions(
id_reuse_policy=IdReusePolicy.DISALLOW,
request_id=f"start-example-flow:{input_num}:{flow_id}",
ignore_already_started=True,
).with_attribute(status, "queued")
await client.start_flow(example_flow, flow_id, input_num, options)
return flow_id

Example: examples/python/dex_examples/primitives/flow/controller.py

For example, starting invoice-42 with input 7 sends RequestID start-example-flow:7:invoice-42. Retrying that call returns the same execution, even after it closes, while the backend retains it. Starting the same Flow ID with input 8 sends a different RequestID and returns an already-started error. No snapshot comparison is needed on the successful path.

Wait when the response promises a business milestone​

If the API must confirm an important operation, call WaitForAttributeMatch or WaitForStepCompletion immediately after StartFlow succeeds, including matching-request deduplication. Deduplication confirms the same start was accepted; it does not prove the operation has finished. No identity-check read is needed between the start and the wait.

For example, an API may promise that a row has been committed to a database table used as the business source of truth. The owning Flow writes that row in Execute, then records a committed Attribute or completes the persistence Step. The API waits for that committed Attribute value or the specific StepExecutionID before returning confirmed persistence. Publish the marker only after the database commit succeeds. WaitForStepCompletion confirms that Step completed; it does not return its output or wait for the whole Flow to finish.

API → StartFlow succeeds or deduplicates → wait for the required commit milestone
Flow Execute → commit business row to database → record committed Attribute / complete Step
API → milestone wait succeeds → return confirmed persistence

Wait only for the condition that establishes the response's promised outcome. If the response only acknowledges accepted background work, return immediately after the start without a wait. Bound required waits with a caller deadline or request timeout. A wait timeout does not undo the start or prove the database write failed; report completion as pending or unknown according to the API contract and preserve the same start identity for retries.

See Step completion waits and Attribute-match waits for the language APIs. Attribute waits target an active run. If a Flow closes before the wait attaches, reconcile the actual wait error through the authoritative business record or a supported terminal read; do not assume the Attribute wait can query a closed run.

Read only when the error outcome needs it​

Error handling follows the business contract. If an already-started error can simply become ALREADY_EXISTS, return that conflict directly. Do not add a read-only RPC just because the error occurred. The helpers above preserve the typed error for that boundary translation.

Read existing state only when it changes the response or next business decision. For example, an API that explicitly permits continuing an existing operation may call its typed read-only RPC after the conflict to decide whether that operation satisfies the request. An API that promises a current snapshot must read current state; it cannot label the submitted initial Attributes as that snapshot. An accepted start does not prove business completion.

SituationAction
Start accepted; response only acknowledges acceptanceReturn the Flow ID and the agreed acceptance fields. No snapshot read.
Start accepted or deduplicated; response promises a critical operationWait for the committed Attribute condition or selected Step completion, then return that milestone outcome.
Already started; business accepts ALREADY_EXISTSReturn the conflict. No snapshot read.
Already started; business permits resuming an existing operationRead its typed snapshot only to decide whether that contract permits resuming it.
Response lost; start acceptance is unknownRetry within a bounded policy with the same identity and options, or read authoritative state if it can establish the outcome.

Do not retry a proven conflict indefinitely. If no stable RequestID was supplied, separate SDK calls use fresh IDs; enabling the ignore option alone does not deduplicate them.

Put dependent starts inside the owning Flow​

For one business operation, the API starts one owning Flow. Its durable Steps perform the follow-on work. Do not start the first Flow, read its status, and start a dependent Flow in the HTTP handler. If that process exits between the starts, no durable Step owns the missing launch.

Use Steps when the work shares one identity and lifecycle. When it needs a separate top-level Flow with its own owner and lifecycle, launch it from an Execute Step with stable downstream Flow ID, RequestID, and start options. Persist the launch input in the owning Flow. If the Worker dies after the downstream start is accepted but before the Step commits, the Step can retry the same start without creating another execution. Keep WaitFor for durable conditions. A separately owned top-level Flow does not need to become a SubFlow merely because another Flow starts it.

API → StartFlow for the owning operation → accepted response
Owning Flow Execute → StartFlow for separately owned work → durable progress
Worker replacement → retry the same downstream start identity → no duplicate launch

RetryPolicy​

server-zd does not support a Flow-level RetryPolicy. A start request that sets one fails with INVALID_ARGUMENT. Tracked in issue 996. To retry work, use Step retry, which is supported.

StartDelay​

server-zd does not support StartDelay. A start request with a positive delay fails with INVALID_ARGUMENT. Tracked in issue 995. To delay the first work, start the Flow without a delay and begin the StartStep with a durable Timer wait.

Attributes​

Attributes write initial values . Each value must match an Attribute or Attribute map in the Flow persistence schema.

FlowConfig​

Use ConfigOverride in StartFlowOptions to change the initial configuration for one Flow. It changes only the fields you provide. It does not replace the whole FlowConfig.

At start, Dex applies the supplied fields over the Dex Server defaults. Omitted fields keep their default values.

FieldBehavior
ActiveStepSearchModeIndex every active Step, only Steps that ran WaitFor, or no active Steps. The built-in server default does not index active Steps. Set an explicit mode to opt in.
ContinueAsNewThresholdDeprecated. Dex does not continue Flows as new. Dex rejects a negative value, stores the value in the FlowConfig, and otherwise ignores it. ContinueAsNewPageSizeBytes behaves the same way.
StepDurabilityDefault durability for methods without a Step override. Dex resolves a method override first, then this value, then SYNC.
AttributeStoreNamesDex Server Attribute Stores for Attributes that opt in to attribute sync

Update FlowConfig with a Client​

Call UpdateFlowConfig while the Flow is active. Send only the fields you want to change. They will be merged with the existing configuration. The SDK UpdateFlowConfig call does not change the Flow's Worker routing.