Flows
A flow is stateful orchestration triggered by a signal. Flows coordinate sequences of action calls, support retries, and are the right model for anything that needs to outlive a single request.
Defining a flow
domain:
flows:
rejection_followup:
description: "Notify and audit a rejected message"
on: [message.rejected]
correlate_by:
var: payload.user_id
idempotency:
key: { var: payload.id }
policy: dedupe
steps:
- run: guestbook.log_rejection
with:
id: { $expr: { var: payload.id } }
reason: { $expr: { var: payload.reason } }
- run: guestbook.notify_rejection
with:
id: { $expr: { var: payload.id } }
retry:
max_attempts: 3
backoff_ms: 500
Signal triggers
The on field lists the signal IDs that trigger this flow. A single flow can react to multiple signals.
on: [message.created, message.rejected]
When the flow runs, payload.* refers to the signal payload that triggered the current execution. If a flow is triggered by multiple signal types, each when branch is implicit — the flow always runs from its first step for each triggering signal.
Correlation
correlate_by groups flow executions by a value from the signal payload. All executions with the same correlation key share state and are serialized by the flow runtime.
correlate_by:
var: payload.user_id
This makes it safe to run multiple signals for the same user without concurrent state conflicts.
Idempotency
The idempotency block prevents duplicate executions from the same signal.
idempotency:
key: { var: payload.id }
policy: dedupe
policy: dedupe means a second execution with the same key is silently dropped. The key is derived from the signal payload using a var expression.
Steps
Each step in the do block calls an internal action by name. Steps run sequentially. If a step fails and has no retry block, the flow fails and the error is logged.
steps:
- run: guestbook.log_rejection
with:
id: { $expr: { var: payload.id } }
reason: { $expr: { var: payload.reason } }
- run: guestbook.notify_rejection
with:
id: { $expr: { var: payload.id } }
retry:
max_attempts: 3
backoff_ms: 500
The with map passes inputs to the action. Inputs are resolved using $expr from the signal payload context.
Lifecycle constraints
lifecycle limits the resources a flow can consume across its executions.
lifecycle:
state_ttl_seconds: 86400
max_idempotency_keys: 10000
max_runs_per_correlation: 100
| Field | Description |
|---|---|
state_ttl_seconds | How long per-correlation state is retained after the last execution |
max_idempotency_keys | Cap on deduplication key storage per flow definition |
max_runs_per_correlation | Hard cap on invocations per correlation key; acts as a circuit breaker |
What flows may not do
Flows call actions only. A flow cannot call a provider capability directly — that is an action's responsibility. This boundary is enforced at compilation.
Flows also cannot emit signals directly. Signals are emitted by actions. If a flow needs to produce a signal, it does so by calling an action that emits it.
If you only need fire-and-forget async behavior after an action (for example, sending a notification without blocking the response), prefer an action with on_signal_error: log_and_continue rather than a flow. Flows are for stateful, multi-step processes that correlate state across multiple signals.