Split and aggregate
A split step opens an item region and an aggregate step closes it. Every step between them runs once per item: an agent is asked once per ticket, a model is called once per chunk, a person answers once per item. The aggregate waits until its policy is met, then hands every item’s result on as one list.
Use a region when each item needs more than one step, or a step that waits (an agent, a person, a connector action). For a single model call per list entry, the model step’s each option is simpler: see Steps: Model.
How a region runs
trigger → split ──→ classify → reply ──→ aggregate → output (once per item, up to 8 at a time)- The split reads a list from the carried item, for example
$json.tickets. - Each list entry becomes an item. A step inside the region receives the carried item with the entry’s fields spread over it, plus
itemIndex(0-based position in the list) anditemKey. An entry that is not an object arrives as$json.item. - The list field itself is removed from what each item carries, so 200 tickets are not copied into every per-item step.
- Each item walks the region independently until it reaches the aggregate.
- When the aggregate’s policy is met, the region closes and the run continues once, with the results under the aggregate’s
intofield.

Split parameters
| Param | Builder label | Values | Default | What it does |
|---|---|---|---|---|
source |
Items from: A list on the item, then List field | { list: "<field>" } |
required | The dot path of the list to walk, read from the carried item. |
mode |
Items run: One at a time / Several at once | "sequential" or "parallel" |
"sequential" |
Sequential walks one item at a time. Parallel walks up to 8 items at once per region. |
An empty list closes the region at once and the aggregate hands on zero results.
Aggregate parameters
| Param | Builder label | Values | Default | What it does |
|---|---|---|---|---|
split |
Closes | a split step’s id | required | The split this aggregate closes. Each split has exactly one aggregate. |
policy |
Carries on when | "all", "any", { atLeast: n } |
"all" |
When the region closes: every item finished, the first item done, or n items done. |
into |
Save items as | a field name | "items" |
Where the results land on the carried item. |
The aggregate writes this object under into:
{ "items": [ { "itemKey": "split#1#0", "itemIndex": 0, "state": "done", "result": { "category": "billing" } }, { "itemKey": "split#1#1", "itemIndex": 1, "state": "done", "result": { "category": "bug" } } ], "reason": "complete", "count": 2, "total": 2}result is the carried item as it reached the aggregate. count is how many items finished done; total is how many items the list held. Results are in list order. When a policy closes the region early (any, atLeast), items still queued or running are listed with their state and no result.
Example
Triage a batch of tickets: one model call and one cubby write per ticket, several tickets at once.
nodes: [ { id: "batch", kind: "trigger" }, { id: "each-ticket", kind: "split", params: { source: { list: "tickets" }, mode: "parallel" } }, { id: "classify", kind: "model", params: { alias: "llm", input: { messages: [{ role: "user", content: "=Classify as billing, bug or question:\n{{ $json.text }}" }], max_tokens: 16, }, into: "classification", }, }, { id: "save", kind: "cubbyExec", params: { alias: "triage", sql: "INSERT INTO triage_tickets (run_id, ticket_id, category) VALUES ('{{ $runId }}', ?, ?)", args: ["={{ $json.id }}", "={{ $json.classification.text }}"], }, }, { id: "collect", kind: "aggregate", params: { split: "each-ticket", policy: "all", into: "triaged" } }, { id: "done", kind: "output" },],edges: [ { from: "batch", to: "each-ticket" }, { from: "each-ticket", to: "classify" }, { from: "classify", to: "save" }, { from: "save", to: "collect" }, { from: "collect", to: "done" },],Start it with a payload like { "tickets": [{ "id": "T-1", "text": "I was charged twice" }, { "id": "T-2", "text": "The export button does nothing" }] }.
Per-item waits
Agent, connector action, and person steps inside a region wait per item. Each item’s answer is matched to that item, so answers can arrive in any order. A loop edge inside a region counts per item, which is how you retry one item without touching the others.
A person’s step inside a region:
- must be an assigned step (it declares
assignees,policy,options, or another assigned-step param), so each answer names its item. See People in workflows. - is allowed only in a sequential region. In a parallel region every item would hold a person’s wait open at once.
Failures
A failed item fails the run. The aggregate’s onItemError param accepts only "fail-run", which is also the default.
Rules the graph must follow
cef build and the runner refuse a graph that breaks any of these, before the run has any effect:
| Rule | Why |
|---|---|
Every split has exactly one aggregate whose split names it. |
One region, one place where items come back. |
| No split inside a region. | Nested regions are not supported. |
No join inside a region. |
An item’s branches would join across items. |
| No trigger inside a region. | It would start runs from inside one item. |
| Edges enter a region only through its split and leave only through its aggregate. | An item that leaves by a side door never reaches the aggregate, so the region never closes. |
| A loop edge may not cross the region boundary. | Same reason. |
| The split has at least one edge out. | Otherwise no item is walked. |
Limits
| What | Limit |
|---|---|
| Items in flight per region, parallel mode | 8 |
| Items in flight, sequential mode | 1 |
| Nested regions | not supported |
onItemError |
fail-run only |