Parallel Execution
Run a small set of independent workflow branches concurrently, durably, and with one deterministic parent continuation.
Durable branches
Parallel is a structured fan-out/fan-in node. The parent pauses as awaiting_parallel; a durable coordinator admits isolated branch items up to the configured concurrency and resumes the parent exactly once.
{
"type": "Parallel", "id": "enrich",
"execution": { "mode": "durable", "max_concurrency": 2 },
"branches": [
{ "id": "crm", "children": [{ "type": "Tool", "id": "lookup", "tool": "crm_lookup" }] },
{ "id": "profile", "children": [{ "type": "Code", "id": "score", "expression": "function () { return {}; }" }] }
]
}Completed branch outputs are available under their branch IDs, for example {{enrich.crm.lookup}}. Branches read frozen parent state and cannot read sibling-local state.
Bulk child calls
WorkflowCall.for_each expands one pinned child workflow deployment across a parent array. The collection is evaluated once, each item receives isolated aliases while its input is resolved, and the parent resumes with results in source order.
{
"type": "WorkflowCall", "id": "score-leads", "workflow_id": "lead-scorer",
"for_each": { "items": "{{leads}}", "item": "lead", "index": "index" },
"input": { "lead": "{{lead}}", "position": "{{index}}" },
"join": { "mode": "all" },
"failure": { "mode": "collect" },
"execution": { "mode": "durable", "max_concurrency": 10 }
}Bulk calls are always awaited. Durable batches create pinned child runs; ephemeral batches run eligible synchronous callees inline and never create child runs or fan-out rows. The result shape is { items: [{ index, status, output, error }], __join__: { total, succeeded, failed, cancelled } }. Retrying applies only to durable items.
The canonical bulk-contact-enrichment-100.json example generates a deterministic batch and admits ten pinned child calls at a time.
Joins and failures
| Setting | Meaning |
|---|---|
all | Wait for every successful branch. |
any | Continue after the first success; unstarted losers are cancelled. |
n | Continue after a configured number of successes. |
fail_fast | Fail when the success quorum cannot be reached. |
collect / skip | Only with all: wait for all items despite failures. |
Inspecting runs
The dashboard exposes fan-out progress on the run. Operators can also use the management API, CLI, or MCP tools.
chatterfly runs fanouts <run-id>
chatterfly runs retry-item <run-id> <fanout-id> <item-id> --confirm
chatterfly runs cancel-item <run-id> <fanout-id> <item-id> --confirm
chatterfly runs retry-items <run-id> <fanout-id> <item-id>... --confirm
chatterfly runs cancel-items <run-id> <fanout-id> <item-id>... --confirmRetrying a failed or cancelled item restarts its branch from the frozen parent snapshot. Previous checkpoints, awaited-child references, output, and errors are cleared.
MCP offers get_run_fanouts and paginated get_run_fanout_items; pass its optional status filter to page active, completed, failed, or cancelled items before the cursor is applied. It also provides confirmation-gated single-item and explicit batch retry/cancel tools. Batch actions allow one to 100 items and reject the entire request when any item is ineligible.
Current limits
Operators can pause new admissions while preserving queued items with tenant setting fanout_enabled: false, or pause selected workflows with fanout_disabled_workflow_ids. Blocked queues retry admission every minute and resume when the setting is re-enabled.
WorkflowCall.for_each supports up to 1,000 source items, 100 concurrent calls, 64 KiB of resolved input per item, and a 1 MiB aggregate result. execution.mode: "ephemeral" is limited to synchronous callees and requires explicit at-least-once acknowledgement because accepted API calls can repeat after a worker restart. Connection-backed provider calls are capped per process at 20 per provider and 5 per concrete connection; mutations without a provider idempotency header are rejected, and supported operations receive a stable item key unless the workflow supplies one. Its aggregate metrics can be suppressed with observability: "none", but it never persists live item progress. Its result can collect ordered envelopes or discard them after aggregation. See the workflow DSL reference for the broader node model.
