π Verb Reference
This page documents all 31 saga step types ("verbs") supported by the engine.
Quick-reference tableβ
| Verb | License group | One-liner |
|---|---|---|
action | common | Dispatch a step to an external worker and pause until it replies. |
decision | common | Evaluate a stored rule table and branch on its output. |
switch | common | Evaluate a CEL expression and branch on the string result. |
error | common | Immediately fail the saga with a code and message. |
noop | common | Placeholder β do nothing and advance. |
end | (terminal) | End the run as succeeded. |
set_var | common | Write a literal or CEL-computed value into a variable. |
transform | common | Evaluate a CEL expression and store the result. |
merge | common | Merge a CEL-evaluated map into an existing variable. |
filter | common | Keep list elements that satisfy a CEL predicate. |
map | common | Transform each element of a list with a CEL expression. |
assert | common | Fail the saga if a CEL expression is not truthy. |
log | common | Emit a structured log line at a chosen level. |
metric_emit | observability | Append a named metric event to the run's audit stream. |
http_request | common / external_io_advancedΒΉ | Issue a synchronous outbound HTTP request. |
webhook_emit | external_io_advanced | POST a JSON payload to an external URL. |
wait_duration | waits | Pause for a Go duration (e.g. "5s", "1h30m"). |
wait_until | waits | Pause until an RFC3339 absolute timestamp. |
wait_for_signal | events_and_signals | Pause until a named external signal arrives (with optional timeout). |
wait_for_event | events_and_signals | Pause until a matching event topic + header subset arrives. |
emit_signal | events_and_signals | Send a signal to another (or the same) run. |
emit_event | events_and_signals | Publish an event via the configured EventEmitter. |
while | loops_and_recovery | Loop while a CEL condition holds; exit via branching. |
try_catch | loops_and_recovery | Push an error-handler frame; jump to catch on any step error. |
cancel | loops_and_recovery | Cancel a run (self or a target). |
parallel | parallel_control | Fan out N branches and join when a strategy is satisfied. |
join | parallel_control | Barrier: reconvene independently-spawned upstream streams before continuing. |
foreach | parallel_control | Fan out one branch per element of a CEL-evaluated list. |
sub_saga | compositions | Start a child workflow and pause the parent until it finishes. |
spawn_saga | compositions | Fire-and-forget: start a child workflow and continue immediately. |
manual_approval | human_interaction | Create a user task; pause until an assignee submits it. |
collect_input | human_interaction | Like manual_approval but form_schema is required. |
ΒΉ
http_requestiscommonwhen method isGETwith nosecret_ref; all other configurations require theexternal_io_advancedgroup.
π‘ License groups gate verbs at publish time and at runtime. In development the
StubAllowAlllicensing resolver (used bysaga.InMemory()andsaga.Newwith noLicensingset) permits every group. In production, override per-request with theX-Feature-Overrideheader or theFeatureOverridesfield on the run start request.
Core / Controlβ
actionβ
Dispatches the step to a named worker process and pauses the saga until the worker replies.
| Input | Required | Notes |
|---|---|---|
step.Action | β | "service.name" β must contain a dot. Identifies the worker queue. |
step.Inputs | optional | Forwarded verbatim to the worker as inputs. |
Output: The worker's result map is merged directly into Variables. There is no out_var β whatever keys the worker returns become top-level variables.
β οΈ In embedded mode (
saga.InMemory()) the action verb publishes to an in-process publisher. You still need a worker goroutine (or service mode) to actually handle the dispatch; a plain in-memory saga with no registered worker handler will leave the run paused indefinitely.
Example: examples/workflows/action.json
decisionβ
Evaluates a stored decision-table rule and returns its output map. The engine reads result["branch"] to pick a route from step.Branches.
| Input | Required | Notes |
|---|---|---|
rule_id | β | Stable ID of a published RuleDefinition. |
inputs_map | optional | map[string]string β maps rule input keys to variable names. Omit to pass Variables directly. |
Output: The rule's full output map (including branch) is merged into Variables.
Example: examples/workflows/decision.json
switchβ
Evaluates a CEL expression to a string and routes via step.Branches. Simpler than decision when no rule table is needed.
| Input | Required | Notes |
|---|---|---|
expr | β | CEL expression over Variables; must produce a string. |
Output: {"branch": "<result>"} β the engine picks step.Branches[<result>].Next. An unknown branch value is a runtime error.
Example: examples/workflows/switch.json
errorβ
Immediately fails the saga as a non-retryable error.
| Input | Required | Notes |
|---|---|---|
code | β | Error code string surfaced in the run's error record. |
message | optional | Human-readable description. |
Output: None β the run terminates as failed.
Example: examples/workflows/error.json
noopβ
Does nothing. Advances to step.Next. Useful as a placeholder during development or as a join point for multiple branches.
Inputs: none. Output: empty map.
Example: examples/workflows/noop.json
endβ
Marks the run as succeeded. Every workflow needs at least one end step. It is dispatched inline by the engine (not via a queue) so no license group applies.
Inputs: none. Output: none β the saga terminates.
Dataβ
set_varβ
Writes a value to a variable. Use it to seed variables before CEL verbs read them.
| Input | Required | Notes |
|---|---|---|
out_var | β | Destination variable name. Dotted keys (a.b.c) write into nested maps. |
value | one of value/expr | Literal value β passed through unchanged. |
expr | one of value/expr | CEL expression over Variables; result is written. expr wins when both are present. |
Output: {out_var: <value>}.
Example: examples/workflows/set_var.json
transformβ
Evaluates a CEL expression and writes the result to a named variable. Equivalent to set_var with expr.
| Input | Required | Notes |
|---|---|---|
expr | β | CEL expression over Variables. |
out_var | β | Destination variable name. |
Output: {out_var: <result>}.
Example: examples/workflows/transform.json
mergeβ
Evaluates a CEL expression that must produce a map, then deep-merges it into an existing variable.
| Input | Required | Notes |
|---|---|---|
from | β | CEL expression β map. |
into | β | Name of the destination variable. Dotted paths are supported. |
Output: The merged variable value under its existing key.
Example: examples/workflows/merge.json
filterβ
Keeps list elements where a CEL predicate is truthy.
| Input | Required | Notes |
|---|---|---|
list | β | CEL expression β list. |
expr | β | CEL predicate; the current element is bound as _. |
out_var | β | Variable to write the filtered list to. |
Output: {out_var: [filtered list]}.
Example: examples/workflows/filter.json
mapβ
Transforms every element of a list with a CEL expression.
| Input | Required | Notes |
|---|---|---|
list | β | CEL expression β list. |
expr | β | CEL transform; element bound as _. |
out_var | β | Variable to write the mapped list to. |
Output: {out_var: [mapped list]}.
Example: examples/workflows/map.json
assertβ
Fails the saga if a CEL expression is not truthy. Use it for invariant checks mid-workflow.
| Input | Required | Notes |
|---|---|---|
expr | β | CEL boolean expression. |
code | optional | Error code emitted on failure (default "assertion_failed"). |
Output: Empty map on success; non-retryable error on failure.
Example: examples/workflows/assert.json
Observabilityβ
logβ
Emits a structured log line via the engine's logger.
| Input | Required | Notes |
|---|---|---|
message | β | Log message string. |
level | optional | "info" (default) | "warn" | "error". |
Output: Empty map.
Example: examples/workflows/log.json
metric_emitβ
Appends a named metric event to the run's audit stream. (Prometheus side-channel wiring is planned for a future release.)
| Input | Required | Notes |
|---|---|---|
name | β | Metric name string. |
value | β | Numeric value. |
labels | optional | map[string]string of label key/value pairs. |
Output: Empty map.
Example: examples/workflows/metric_emit.json
I/Oβ
http_requestβ
Issues a synchronous outbound HTTP request and merges the response into Variables.
| Input | Required | Notes |
|---|---|---|
url | β | Target URL. |
method | optional | HTTP method (default "GET"). |
headers | optional | map[string]any β request headers. |
body | optional | Any value; JSON-marshalled into the request body. |
timeout_s | optional | Request timeout in seconds (default 30). |
secret_ref | optional | Secret key resolved via the Secrets resolver β set as Authorization header. |
out_var | optional | Output prefix (default "http_result"). |
Output keys (with out_var = "http_result"):
http_resultβ parsed JSON body (or raw string if non-JSON).http_result_statusβint64HTTP status code.http_result_headersβmap[string]stringof response headers.
β οΈ License group is
commononly forGETwith nosecret_ref. Any other method or authenticated request requires theexternal_io_advancedgroup.
Example: examples/workflows/http_request.json
webhook_emitβ
POSTs a JSON payload to an external URL, with optional HMAC-SHA256 request signing.
| Input | Required | Notes |
|---|---|---|
url | β | Target URL. |
body | β | Any value; JSON-marshalled. |
secret_ref | optional | Secret key β X-Webhook-Sig: sha256=<hex> header. |
timeout_s | optional | Timeout in seconds (default 15). |
headers | optional | Additional request headers. |
async | optional | bool; default false. When true, fires the request in a goroutine and returns immediately (failures are logged only). |
out_var | optional | Output prefix (default "webhook_result"). |
Output (sync mode): {out_var}_status β int64 HTTP status code.
Output (async mode): {out_var}_async: true.
Example: examples/workflows/webhook_emit.json
Timingβ
wait_durationβ
Pauses the saga for a duration expressed as a Go duration string.
| Input | Required | Notes |
|---|---|---|
duration | β | Go duration string e.g. "5s", "1h30m", "72h". |
The engine's timer dispatcher wakes the run when the deadline passes.
Example: examples/workflows/wait_duration.json
wait_untilβ
Pauses the saga until an absolute point in time.
| Input | Required | Notes |
|---|---|---|
timestamp | β | RFC3339 datetime string, e.g. "2026-01-01T09:00:00Z". |
Example: examples/workflows/wait_until.json
Events & Signalsβ
wait_for_signalβ
Pauses the saga until a named external signal arrives via POST /api/v1/sagas/{run_id}/signal/{name}.
| Input | Required | Notes |
|---|---|---|
name | β | Signal name to await. |
timeout_s | optional | Max seconds to wait. Omit to wait indefinitely. |
Timeout routing: when the deadline fires while the signal has not yet arrived, the engine checks step.Branches["timeout"].Next first. If that branch exists, the run routes there instead of step.Next β handy for escalation paths.
π‘ Wire a
timeoutbranch to an escalation step to handle missed approvals or SLA breaches without any extra polling.
Example: examples/workflows/wait_for_signal.json
wait_for_eventβ
Pauses the saga until an event with a matching topic (and optional header subset) arrives via the event bus.
| Input | Required | Notes |
|---|---|---|
topic | β | Event topic / RabbitMQ routing key to await. |
headers | optional | map[string]any β incoming event headers must contain all of these key/value pairs (string equality). |
timeout_s | optional | Number of seconds to wait. On timeout the run routes to the step's timeout branch if defined, else to next. Omitted = wait indefinitely. |
π‘ Add a
"timeout"entry to the step'sbranchesto escalate when no matching event arrives beforetimeout_s(same pattern aswait_for_signal).
Example: examples/workflows/wait_for_event.json
emit_signalβ
Sends a signal to another run (the send-side complement of wait_for_signal). If the target is currently paused awaiting that signal, it is consumed and the target advances immediately.
| Input | Required | Notes |
|---|---|---|
run_id | β | UUID of the target run. |
name | β | Signal name. |
payload | optional | map[string]any carried with the signal. |
Output: Empty map.
Example: examples/workflows/emit_signal.json
emit_eventβ
Publishes an event via the configured EventEmitter (in-process when embedded; RabbitMQ in service mode).
| Input | Required | Notes |
|---|---|---|
topic | β | Event topic / routing key. |
headers | optional | map[string]any β map[string]string. |
payload | optional | map[string]any event payload. |
Output: Empty map.
π‘ In embedded mode the in-process emitter both wakes runs awaiting the topic and runs the trigger dispatcher, so matching
record_transitiontriggers start new runs β parity with service mode (no broker needed).
Example: examples/workflows/emit_event.json
Loops & Recoveryβ
whileβ
Loops while a CEL condition evaluates to true.
| Input | Required | Notes |
|---|---|---|
condition | β | CEL boolean expression over Variables. |
max_iterations | optional | Default 100; hard cap 10000. Prevents runaway loops. |
Output: {"branch": "continue" | "exit"}.
Wiring pattern:
step.Branches.continue β nextβ first step of the loop body.step.Branches.exit β nextβ first step after the loop.- The body's last step sets
nextback to thewhilestep.
An iteration counter is maintained at Variables._while.<step_id>.iter.
Example: examples/workflows/while.json
try_catchβ
Pushes an error-handler frame. If any step inside the protected region errors, the saga jumps to the catch step instead of failing. The error context is written to Variables._error.
| Input | Required | Notes |
|---|---|---|
try | β | []string of step IDs in the protected region. Used by ValidateDefinition to reject disallowed nesting (e.g. parallel inside try). |
catch | β | Step ID to jump to on error. |
Wiring: set step.Next to the first step inside the try body. The body's last step sets next to whatever comes after the protected region.
Example: examples/workflows/try_catch.json
cancelβ
Cancels a run.
| Input | Required | Notes |
|---|---|---|
run_id | optional | UUID of the target run. Omit (or set to the current run's ID) to self-cancel β the current run ends as cancelled. |
reason | optional | Human-readable reason string. |
Self-cancel: returns ErrSagaCancelled; the run ends immediately as cancelled.
Target-cancel: cancels the target run and the current run continues to step.Next.
β οΈ When you cancel a run that is a child of a
paralleljoin, the join may be left waiting if the join strategy expects all children. The cancelled child is counted as terminal, so withjoin_strategy: "all"the parent will eventually time out or remain paused unless all other children also complete.
Example: examples/workflows/cancel.json
Parallelismβ
parallelβ
Fans out N branches as child runs and pauses the parent until the join strategy is satisfied.
| Input | Required | Notes |
|---|---|---|
branches | β | []any of branch objects or a CEL string β list. Each branch: long-form {"start": "step_id", "steps": [...]} or short-form {"type": "...", "inputs": {...}}. |
join_strategy | optional | "all" (default) β wait for every branch. "quorum" β wake after quorum_n successes. |
quorum_n | required when quorum | Positive integer β€ branch count. Can also be a CEL string evaluated at runtime. |
Output: Each branch's variables are aggregated into Variables._parallel.<step_id>.branches when the parent wakes.
β οΈ Remaining branches continue to run after a quorum wake β they are not cancelled.
Example: examples/workflows/parallel.json
joinβ
A barrier that reconvenes streams an earlier step spawned independently, before the run continues. Where parallel spawns its own branches and pauses on the same step, join waits on children that previous steps spawned in the same run β the natural producer is spawn_saga, whose fire-and-forget children the run did not wait on. join lets a later step gather those streams back together.
| Input | Required | Notes |
|---|---|---|
streams | β | []string of upstream step IDs in this run whose spawned children the join waits on (or a CEL string β list of step IDs). Each named step must have spawned at least one child (spawn_saga, parallel, foreach, or sub_saga); the join watches the union of their children. |
join_strategy | optional | "all" (default) β wait for every watched child to reach a terminal state. "quorum" β resolve once quorum_n watched children have succeeded. |
quorum_n | required when quorum | Positive integer β€ the watched-child count. Can also be a CEL string evaluated at runtime. |
Resolution: If every/quorum watched child is already terminal when the join runs, it aggregates and continues without pausing. Otherwise it pauses; the engine re-evaluates the barrier whenever a watched child terminates and wakes the run once the strategy is met.
Output: Each watched child's variables are aggregated into Variables._join.<step_id>.branches (same {key, variables, state, _user_task?} shape as parallel).
β οΈ A misconfigured barrier fails fast:
streamsnaming a step that spawned no children, orquorum_nexceeding the watched-child count, errors at the step rather than pausing forever. Remaining children continue to run after a quorum wake β they are not cancelled.
Example: examples/workflows/join.json
foreachβ
Fans out one child run per element of a CEL-evaluated list (parallel mode only in v1).
| Input | Required | Notes |
|---|---|---|
list | β | CEL expression β list. |
body | β | []any of step objects forming the loop body. |
start | β | ID of the first step inside body. |
parallel | optional | bool; default true. Sequential mode is not yet supported β use while with an index counter for sequential loops. |
Each child run receives Variables._foreach_item (the element) and Variables._foreach_index (zero-based index). An empty list advances without spawning.
Example: examples/workflows/foreach.json
Composition (Call Tree)β
sub_sagaβ
Starts a named workflow as a child saga and pauses the parent until the child reaches a terminal state.
| Input | Required | Notes |
|---|---|---|
workflow_id | β | Stable ID of the child WorkflowDefinition. |
inputs | optional | map[string]any passed as the child's initial inputs. |
entrypoint | optional | Named entry point on the child definition (see Entrypoints). Defaults to Start. |
Output: Empty map on parent resume (child variables are not automatically merged; wire a set_var/transform after if needed).
Example: examples/workflows/sub_saga.json
spawn_sagaβ
Starts a named workflow as a fire-and-forget child. The parent continues immediately to step.Next without waiting.
| Input | Required | Notes |
|---|---|---|
workflow_id | β | Stable ID of the child workflow. |
inputs | optional | map[string]any passed as the child's initial inputs. |
entrypoint | optional | Named entry point on the child. Defaults to Start. |
Output: Empty map; parent is not paused.
Example: examples/workflows/spawn_saga.json
Human Interactionβ
manual_approvalβ
Creates a user task and pauses the saga until the assignee submits it via POST /api/v1/sagas/{run_id}/user_task/{task_id}/submit.
| Input | Required | Notes |
|---|---|---|
assignee | β | User ID or role expected to submit. |
due_in | optional | Go duration string β sets due_at = now + due_in. |
form_schema | optional | map[string]any rendered in the admin UI. |
Output: None on pause; the submitted form data is available after the run resumes.
Example: examples/workflows/manual_approval.json
collect_inputβ
Like manual_approval but form_schema is required. Use this when the workflow needs structured data from the user (remediation notes, parameters, etc.) rather than a simple approve/reject.
| Input | Required | Notes |
|---|---|---|
assignee | β | User ID or role. |
form_schema | β | map[string]any schema β must be non-empty. |
due_in | optional | Go duration deadline. |
Output: None on pause.