Loop
The loop primitive has two modes: foreach for deterministic batch processing and react for AI agent reasoning. Both share the same type: "loop". The mode property determines behavior.
Foreach Mode
Process an array of items with configurable concurrency and failure handling. Foreach is designed for bounded inline work. Use it with custom-table steps or registered HTTP actions that you explicitly mark as inline.
{
"type": "loop",
"properties": {
"mode": "foreach",
"items_path": "@input.orders",
"item_variable_name": "order",
"actions_to_execute": [
{
"type": "custom-table",
"properties": {
"table": "order_work_queue",
"operation": "upsert",
"key_fields": ["order_id"],
"keys": ["order_id", "customer_id", "amount", "status"],
"values": ["@order.id", "@order.customer_id", "@order.total", "ready"]
}
}
],
"max_concurrency": 10,
"failure_strategy": "continue_on_error",
"collect_results": true,
"result_key": "queuedOrders"
}
}
Foreach Properties
| Property | Type | Required | Description |
|---|---|---|---|
mode |
"foreach" |
Yes | Selects foreach mode |
items_path |
@path / doc: | Yes | Array of items to iterate over. Accepts @path references or doc: uploaded documents (CSV/JSON resolve to arrays) |
item_variable_name |
string | Yes | Variable name for the current item (accessible as @{name}) |
iteration |
string | No | "inline" (default) runs actions_to_execute for each item. "child_run" starts one workflow run per item instead. See Child-Run Iteration |
actions_to_execute |
array | Yes | Inline-safe steps to run for each item, such as custom-table operations and HTTP actions explicitly marked inline: true |
max_concurrency |
number | No | Maximum parallel executions (default: 5, max: 50) |
failure_strategy |
string | No | "continue_on_error" (default) or "fail_fast" |
collect_results |
boolean | No | Whether to gather results from all iterations (default: true) |
result_key |
string | No | Context key for collected results |
Item Access
Inside actions_to_execute, reference the current item using the variable name:
{
"item_variable_name": "order",
"actions_to_execute": [
{
"type": "custom-table",
"properties": {
"table": "order_work_queue",
"operation": "write",
"keys": ["order_id", "amount", "currency"],
"values": ["@order.id", "@order.total", "@order.currency"]
}
}
]
}
Failure Strategies
continue_on_error (default): Failed items are recorded but processing continues. Use when one rejected row should not stop the rest of the batch.
fail_fast: If any item fails, the loop stops and the run fails. Use when all items must succeed.
Child-Run Iteration
A foreach loop with iteration: "child_run" starts one governed run of another workflow for each item, instead of running actions_to_execute inline. Each child is an ordinary workflow run with its own record, so it can pause for an approval or run an agent. The parent run waits with status waiting_on_children and continues from the next step once, when every item has an outcome.
{
"type": "loop",
"properties": {
"mode": "foreach",
"iteration": "child_run",
"workflow": "review-exception-packet",
"items_path": "@packets",
"item_variable_name": "packet",
"max_children": 50,
"max_concurrency": 5,
"max_child_attempts": 2,
"stop_after_consecutive_failures": 3,
"failure_strategy": "continue_on_error",
"result_key": "packetReviews"
}
}
Each child run receives its item as input under item_variable_name, so the child workflow above reads it as @input.packet or @packet.
Child-Run Properties
| Property | Type | Required | Description |
|---|---|---|---|
iteration |
"child_run" |
Yes | Selects child-run iteration. Do not set actions_to_execute |
workflow |
string | Yes | Id or name of the workflow to run for each item |
items_path |
@path | Yes | Array of items. Each item starts one child run |
item_variable_name |
string | Yes | Input key the item is passed under |
max_children |
number | No | Most items the step accepts (default: 50, max: 200) |
max_concurrency |
number | No | Children launched and running at once (default: 5, max: 50). A child paused for a person does not hold a slot, and a child resumed by a decision continues without waiting for one, so right after many decisions more children than this can briefly finish their remaining steps |
child_timeout_ms |
number | No | Deadline for each child, counted from its launch (min: 60000, max: 604800000, seven days). Default: the longest expiry_seconds among the child workflow's approvals plus two hours, or two hours when the child workflow has no approval |
max_child_attempts |
number | No | Most attempts per item, counting the first. Only a failed child is retried (default: 2, max: 3) |
stop_after_consecutive_failures |
number | No | Stop launching after this many failed attempts in a row (default: 3, max: 10) |
failure_strategy |
string | No | "continue_on_error" (default) or "fail_fast" |
result_key |
string | No | Context key for the summary (default: childRunResults) |
timeout_ms and collect_results apply to inline iteration only.
Every PbotApproval in the child workflow, including approvals in branches, must declare expiry_seconds and on_expiry: "reject" (Expiry) so no child can wait forever. The child workflow cannot contain a child-run loop of its own.
Outcomes
Each item ends with one outcome:
completed: the child run completed.appliedandconflictare completed runs whose last custom-table transaction was applied, or met a conflict.rejected: a person rejected an approval in the child.expired: an approval in the child expired without a decision.failed: the child failed for any other reason, after its retries.timed_out: the child was cancelled atchild_timeout_msor at the batch deadline.cancelled: the child was cancelled some other way.not_started: the child never launched because the batch stopped.
Only failed is retried, and only failed counts toward stop_after_consecutive_failures. A rejection or an expiry is a business result for that item, so any number of them in a row never stops the batch.
When every item has an outcome, the step stores a summary under result_key:
{
"status": "needs_attention",
"item_count": 3,
"counts": { "completed": 1, "rejected": 1, "expired": 1 },
"unfinished": [],
"needs_person": [2],
"outcomes": [
{ "item_index": 0, "outcome": "completed", "child_run_id": "run-3f2b1c9e-8d4a-4c1e-9b7a-2e6f5d4c3b2a", "attempts": 1 },
{ "item_index": 1, "outcome": "rejected", "child_run_id": "run-7a1d0e44-52c3-4b8f-a0e6-91c2d3f4e5a6", "attempts": 1 },
{ "item_index": 2, "outcome": "expired", "child_run_id": "run-c0ffee12-3456-4789-8abc-def012345678", "attempts": 1 }
],
"outcomes_truncated": false
}
statusiscompletedwhen every item endedcompleted,applied, orrejected, andneeds_attentionotherwise.needs_attentionis a business result, not an error: the run continues to the next step, which can branch on it.unfinishedlists items that endedfailed,timed_out,cancelled, ornot_started.needs_personlists items that endedexpiredorconflict. Items are listed by index, or bypacket_idwhen the item is an object that has one.haltedis present when the batch stopped early:consecutive_failures,fail_fast, orbatch_deadline.outcomeshas one entry per item, with its latest child run id and the number of attempts.
Each child run records its parent. GET /runs/:runId/status on a child returns parent_run_id, parent_step_index, and parent_item_index.
What Happens When
- There are more items than
max_children. Nothing launches. The step completes withstatus: "needs_attention",refused: "max_children",item_count, andmax_children. - The batch could exceed the organization's LLM token ceiling. Before launching, the step multiplies the number of items by the total token budget of the agent steps in the child workflow (each step's
llm_budget.max_total_tokens, or the default agent budget when it sets none). Above the ceiling (default: 50,000,000 tokens), nothing launches and the result isneeds_attentionwithrefused: "batch_llm_ceiling",batch_llm_tokens, andbatch_llm_ceiling. - The organization is at its limit of active children. By default an organization runs at most 200 active child runs at once, across all parents. Paused children do not count, and launches another waiting parent has planned do; a parent that has ended holds no capacity. Parents plan their launches one at a time per organization, so two parents never spend the same capacity. Launches wait for capacity.
- The item list is empty. The step completes at once with
status: "completed"anditem_count: 0. - A child fails. The item gets a new child run until
max_child_attemptsis used. When failed attempts in a row reachstop_after_consecutive_failures, nothing more launches, running children finish, and the remaining items arenot_started. failure_strategyis"fail_fast". The first item that endsfailedstops the batch: running children are cancelled, the remaining items arenot_started, and the step and the run fail. The run sendsworkflow_run_failedwith reasonchild_run_fail_fast.- A child runs past
child_timeout_ms. It is cancelled and endstimed_out. Time spent paused for a person counts. - The batch deadline passes. The deadline is
child_timeout_msplus two hours from the step's start. Running children are cancelled (timed_out), the remaining items arenot_started, andhaltedisbatch_deadline. - The parent run is cancelled. Its unfinished child runs are cancelled too, including a child that was still starting, and its unfinished items are settled as cancelled.
- The step cannot start. The step and the run fail before anything launches, and the step result carries one of these error codes:
CHILD_RUN_ITEMS_INVALID:items_pathdoes not resolve to an arrayCHILD_RUN_WORKFLOW_REQUIRED:workflowis missing or emptyCHILD_RUN_WORKFLOW_NOT_FOUND: no workflow with that id or name exists in the organizationCHILD_RUN_SELF_REFERENCE: the loop names its own workflowCHILD_RUN_NESTING: the child workflow contains a child-run loopCHILD_RUN_APPROVAL_EXPIRY_REQUIRED: an approval in the child workflow does not declare bothexpiry_secondsandon_expiry: "reject"
React Mode
Run an AI agent that reasons step-by-step toward an objective. See Agents for full details.
{
"type": "loop",
"properties": {
"mode": "react",
"objective": "Investigate this expense report. Check policy compliance, verify receipts, recommend approval or rejection.",
"tools": [
{ "type": "action", "name": "lookup_employee" },
{ "type": "action", "name": "check_expense_policy" },
{ "type": "action", "name": "verify_receipt" }
],
"model": "gpt-4",
"max_iterations": 15,
"timeout_ms": 300000,
"temperature": 0.7,
"on_stuck": {
"action": "escalate",
"iterations": 3
},
"include_reasoning_trace": true,
"result_key": "expenseDecision"
}
}
React Properties
| Property | Type | Required | Description |
|---|---|---|---|
mode |
"react" |
Yes | Selects react mode |
objective |
string | Yes | What the agent should accomplish. Supply the required facts directly, through a doc: reference, or through a declared tool |
tools |
array | Yes | Available tools as typed declarations or action-name strings. See Tool Declarations |
model |
string | No | LLM model to use (default: configured in environment) |
max_iterations |
number | No | Maximum think-act-observe cycles (default: 10) |
timeout_ms |
number | No | Maximum execution time in milliseconds (default: 300000) |
temperature |
number | No | LLM temperature from 0 to 2 (default: 0.7) |
on_stuck |
object | No | Recovery when agent loops without progress |
on_stuck.iterations |
number | No | Repeated iterations before triggering (default: 3) |
on_stuck.action |
string | No | "fail", "escalate", or "retry_with_hint" |
on_stuck.hint |
string | No | Guidance text for retry_with_hint |
include_reasoning_trace |
boolean | No | Include the reasoning trace in the loop result (default: true) |
result_key |
string | No | Context key for the agent's final answer |
When to Use Which Mode
| Use Foreach When | Use React When |
|---|---|
| You know exactly what to do with each item | The task requires judgment or reasoning |
| Processing is deterministic | The approach depends on intermediate results |
| Items are independent of each other | The agent needs to decide what to do next |
| You need parallel processing | You need natural language understanding |
Composing batch work and agent review
actions_to_execute cannot contain another loop. For a governed batch investigation, persist the candidate rows with foreach, then let one top-level ReAct step read them through a fixed custom-table action. If every item needs a separate agent run, put the ReAct step in a child workflow and run it per item with child-run iteration, or start one bounded agent execution per item from your application and put that item's facts or document reference in its objective.
ā Next: Approval