Skip to main content

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
FieldDescription
state_ttl_secondsHow long per-correlation state is retained after the last execution
max_idempotency_keysCap on deduplication key storage per flow definition
max_runs_per_correlationHard 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.

note

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.

What's next

  • Actions: the building blocks that flows orchestrate
  • Signals: the events that trigger flows