Add a map node that runs one node once per item in a list, with a concurrency cap and a policy for what happens when one item fails.
When you need this
Section titled “When you need this”Use a map node whenever a step needs to run once per item in a list you don’t know the size of
ahead of time — a vote from each of several perspectives, one lookup per line item, a check per
attachment. A parallel node’s branches are fixed at design time, one file per branch; a map node
has a single body node that runs once per list item, however many items there turn out to be.
-
Write
<stem>.node.yaml:node: "map", adescription,over,body,on_item_error, andout.concurrencyis optional. -
overis a reference to the list to run over — typically another node’s output, like$prepare.out.perspectives. It has to resolve to a list; anything else fails the node. -
bodynames one sibling node, in a subdirectory named for the map node itself: a nodevotewith body"ballot"keepsballot.node.yamlinvote/next tovote.node.yaml. That node runs once per item. Inside it,$itembinds the current item and$indexits position — both are available only inside a map’s body, the same way$itemisn’t available anywhere else. -
concurrencycaps how many items run at once. Leave it unset and every item starts at once, limited only by how many items there are; set it to hold the number of simultaneous calls down, for example to a model provider’s rate limit. -
on_item_errordecides what one item’s failure does to the rest of the run. Like any other policy slot in AQVEN, it’suse(a built-in name) orrun(your ownmodule:function), pluswithfor whatever parameters the policy takes. Three built-ins ship:skip— the failed item is dropped; every other item keeps running, and the node still succeeds.fail— the first item failure fails the whole node immediately.default, withwith: {value: ...}— the failed item is replaced by that fallback value, so the node still succeeds with every item accounted for.
When the policy skips or replaces an item, the run records that decision as a
map_item_recoveredevent, and the run page shows it: the map’s header gets a “replaced” or “skipped” mark naming the policy, and a replaced item’s output shows the value that went on in its place, labeled as not a model answer. -
outbinds what the node returns, the samename/type/description/fromshape as any other node’s output fields. Two reference forms exist only inside amapnode’sout:$okis the list of every item that finished successfully (a defaulted item counts as successful), in list order, and$failedis the list of items that failed, each as{index, code, message}. Neither is available in the map node’s own body — they only exist once every item is done.
Example
Section titled “Example”The showcase project’s own map node, vote in support_case, binds three of an earlier node’s
outputs into its body alongside the item it’s mapping over. Here’s a small, self-contained map node
instead, checked clean with aqven check: it reads its list straight off the flow’s own input, so
there’s nothing upstream to explain.
each.node.yaml, in flows/digest/nodes/each/:
apiVersion: "aqven/v1"kind: "Node"node: "map"description: "Summarizes each ticket in the batch"over: "$input.tickets"body: "summarize"concurrency: 4on_item_error: use: "skip"out:- name: "summaries" type: "Text[]" description: "Summaries, in ticket order" maxItems: 20 maxLength: 200 from: "$ok[*].summary"- name: "failures" type: "MapItemError[]" description: "Tickets that failed to summarize" maxItems: 20 from: "$failed"over: "$input.tickets" is the flow’s own input list, one item per ticket — no other node’s output to
track down. With concurrency: 4, up to four tickets summarize at once; on_item_error: {use: "skip"}
means one bad ticket doesn’t take the rest down. out uses both reference forms a map node’s out
gets: $ok[*].summary collects the summary field from every ticket that succeeded, in order, and
$failed collects the ones that didn’t, each as {index, code, message} — MapItemError, a built-in
type.
summarize.node.yaml, the body, sits next to each.node.yaml in the same directory, qualified to the
id digest.each__summarize. It’s an ordinary code node — a map node’s body can be any node kind,
not just llm:
apiVersion: "aqven/v1"kind: "Node"node: "code"description: "Summarizes one ticket to its first 200 characters"run: "summarize"in:- name: "ticket" type: "Text" description: "The ticket text for this item" maxLength: 2000 from: "$item"out:- name: "summary" type: "Text" description: "The ticket's summary" maxLength: 200from: "$item" binds the current ticket — the same reference form the showcase’s own ballot.node.yaml
uses inside vote, just without another node’s output bound alongside it. summarize.py, next to this
file, returns summary truncated to 200 characters; nothing about the function itself is specific to
running inside a map node.
See also
Section titled “See also”- How to branch into parallel steps — fixed branches that all run at once, instead of one body node run per list item.
- How to write a step in Python — what
summarize, this example’s body, actually is. - The engineering loop — what to do when a run’s output isn’t what you expected.
- Node specifications — every field on
MapNodeSpec, generated from the code. - Built-in policies and evaluators — every
on_item_errorpolicy’s Python signature, includingdefault’s parameters.