Skip to content

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)
  1. The split reads a list from the carried item, for example $json.tickets.
  2. 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) and itemKey. An entry that is not an object arrives as $json.item.
  3. The list field itself is removed from what each item carries, so 200 tickets are not copied into every per-item step.
  4. Each item walks the region independently until it reaches the aggregate.
  5. When the aggregate’s policy is met, the region closes and the run continues once, with the results under the aggregate’s into field.

A split and aggregate drawn as a region frame around two per-item steps

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