IronFlowDocs

parallel_subworkflows

Execute multiple subworkflows concurrently and collect their results.

Parameters

flowsarrayconditional
Static array of flow configurations to execute in parallel. Use either flows or dynamic flow + source_key.
flowstringconditional
Dynamic fan-out child flow path. Required when flows is omitted.
source_keystringconditional
Context key containing the runtime array to fan out over. Required when flows is omitted.
inputobject
In dynamic mode, authored base input mapping applied to every child run; string values select a matching parent key or remain literals.
item_keystringdefault "item"
Dynamic mode child context key that receives the current source item as a literal value, never a parent-key reference.
index_keystringdefault "index"
Dynamic mode child context key that receives the 1-based source item index.
child_output_keystring
Dynamic mode namespace for each child context inside its result entry; cannot be success, flow, or error.
output_keystringdefault "parallel_results"
Key for the results array in context
on_errorstringdefault "fail_fast"
Error handling: "fail_fast" (fail on any error) or "ignore" (collect all results)
max_concurrentnumberdefault CPU count
Maximum child workflows executing at the same time. Hard-capped at 1024.

Flow Entry Parameters

Each entry in the flows array:

ParameterTypeRequiredDescription
flowstringyesPath to the .lua flow file (relative to _flow_dir)
inputobjectnoContext mapping — keys are child context keys, values are parent context keys or literals. String values are treated as a parent context key when present, otherwise kept as string literals.
output_keystringnoNamespace the child's output under this key in the result entry; cannot be success, flow, or error.

Context Output

KeyTypeDescription
{output_key}arrayArray of result objects, one per flow (in original order)
{output_key}_countnumberTotal number of flows
{output_key}_errorsnumberNumber of flows that failed
{output_key}_all_succeededbooleantrue if all flows succeeded

Each result entry contains:

KeyTypeDescription
successbooleanWhether the subworkflow succeeded
flowstringThe loaded child flow's declared name, or its configured path when loading/execution cannot return a finalized child result
errorstringError message (only present on failure)
context keysanyPublic child context, excluding reserved metadata keys when flattened, or under per-flow output_key

success, flow, and error are reserved at the top level of each result entry. They describe the actual child execution, never values published or inherited by the child. Successful entries have no top-level error field; failed entries have the engine's failure description. Aggregate counts and flags follow those same execution outcomes.

For finalized failed children, error includes unresolved task names and reasons, not just the child status. Summaries use the same redaction, ordering, and limits as subworkflow: at most eight failures, 128 bytes per name, and 768 bytes per message, with truncation markers and an omitted-failure count. Recovered task errors are excluded. Cancelled children or unavailable task details retain a status-only description; loading/execution errors keep their existing error-entry behavior.

on_error = "fail_fast" collects the children before failing the parent step; its aggregate message includes each failed child's reason in input order, using the same text as the corresponding error entry. It does not stop the remaining children at the first failure. These details come from live execution, not a context/history reload.

Without a child namespace, same-named child fields are omitted from the result. To retain them, use a non-reserved per-flow output_key (static mode) or child_output_key (dynamic mode), such as "child". Then result.success remains authoritative while result.child.success, result.child.flow, and result.child.error retain their domain values. Nested fields are not filtered by these metadata names; the existing private-key and redaction rules still apply.

Reserved namespace names are configuration errors, rejected at node execution before any child loads or starts, including an empty dynamic source. They are not suppressed by on_error = "ignore". The node-level output_key names the results array in the parent context and is not subject to this restriction. Existing workflows using a reserved child namespace must rename it; workflows needing flattened domain fields with these names must opt into namespacing.

Both static and dynamic fan-out collect full, redacted live child results with top-level _-prefixed keys removed. IRONFLOW_MAX_TASK_OUTPUT_BYTES bounds persisted task/final inspection snapshots, not the values delivered to parent steps. The existing on_error policy is unchanged: ignoring a failure does not discard values committed before that failure.

This is an in-process handoff, not durable result recovery. Existing child execution limits and parent cancellation/timeouts still apply. Large results remain in memory while collected; prefer artifact references for bulky payloads.

Example

if135_child_error_details.lua uses its bundled helper to demonstrate tolerated child errors without external services. It asserts that a failed result includes the task name and boom: unsupported file type, while a successful result has no error field.

The offline parallel_result_metadata.lua example and its metadata_child.lua helper verify successful and failed static/dynamic children with colliding domain fields, including namespaced preservation.

local flow = Flow.new("parallel_workers")

-- Run three flows concurrently
flow:step("run_all", nodes.parallel_subworkflows({
    flows = {
        { flow = "fetch_users.lua", output_key = "users" },
        { flow = "fetch_orders.lua", output_key = "orders" },
        { flow = "fetch_metrics.lua", output_key = "metrics" }
    },
    max_concurrent = 3
}))

-- Use results from all three
flow:step("summarize", function(ctx)
    local results = ctx.parallel_results
    return {
        all_ok = ctx.parallel_results_all_succeeded,
        count = ctx.parallel_results_count
    }
end):depends_on("run_all")

return flow

With input mapping

flow:step("process", nodes.parallel_subworkflows({
    flows = {
        { flow = "worker.lua", input = { job_id = "job_1" } },
        { flow = "worker.lua", input = { job_id = "job_2" } },
        { flow = "worker.lua", input = { job_id = "job_3" } }
    },
    on_error = "ignore"
}))

Dynamic fan-out from context

Use flow + source_key when the parent workflow discovers work items at runtime. The child flow receives the current item under item and its 1-based position under index by default.

flow:step("run_jobs", nodes.parallel_subworkflows({
    flow = "worker.lua",
    source_key = "jobs",
    input = {
        run_id = "parent_run_id",
    },
    item_key = "job",
    index_key = "job_index",
    child_output_key = "result",
    max_concurrent = 5,
    output_key = "job_results",
}))

If ctx.jobs is:

[
  { "id": "a", "path": "/tmp/a.txt" },
  { "id": "b", "path": "/tmp/b.txt" }
]

then worker.lua runs twice. Each child context receives job, job_index, plus any mapped input fields. Dynamic mode allows an empty source array and returns an empty results array with {output_key}_all_succeeded = true.

Source items and authored mappings have different semantics. If ctx.jobs is { "customer" } and ctx.customer is { id = "C-42" }, the child's item is the string "customer", not that object. An explicit mapping such as input = { customer_copy = "customer" } still selects the parent object. Items are copied without key lookup or interpolation, preserving strings, objects, arrays, booleans, nulls, and numbers within the existing Lua conversion limits. A JSON null reaches a Lua handler as json_null, not nil.

Mapped strings resolve once against a top-level parent key; a selected string is not resolved again. Non-string mapping values are copied without recursively interpreting strings inside objects or arrays. Dynamic mode does not inherit unmapped parent fields. Static entries without input retain their existing parent-context inheritance; input = {} selects no parent fields.

The injected item and numeric one-based index override same-named authored input fields. Choose distinct item_key and index_key: if they are identical, the index retains its existing precedence. Engine-owned context keys, private output filtering, execution-overlay redaction, and resource limits still apply.

parallel_literal_inputs.lua and its literal_item_child.lua helper demonstrate literal dynamic items alongside explicit parent mappings.

Error handling

-- fail_fast (default): step fails if any subworkflow fails
flow:step("strict", nodes.parallel_subworkflows({
    flows = { ... }
}))

-- ignore: collect all results, check errors yourself
flow:step("tolerant", nodes.parallel_subworkflows({
    flows = { ... },
    on_error = "ignore"
}))

IronFlow documentation

Find the next step

Type a keyword to search the documentation.

↑ ↓ to move · Enter to open · Esc to close