Loop and Reuse Flows
How do you repeat invoice processing? Put it in a flow with a typed contract. Run flow calls that flow once. Run flow on list calls a body flow for each item, one after another, and collects the results.
Continue Invoice intake with a Store invoice flow that is already implemented. It routes blank IDs and negative amounts to an invalid-input result before recording. The scaffold adds no validation.
The fixtures below assume a parent with an Invoices input of type list.custom.invoice, and a source node that passes that list through unchanged. Work in the development version. Tests perform real writes and provider actions, so choose development records or an external test destination first.
Define the per-invoice contract
Section titled “Define the per-invoice contract”The new Process invoice body takes one required Invoice. It calls Store invoice with Invoice ID, Amount, and Review needed, then returns invoice_id and status. Use the invoice shape custom.invoice. If the source contains saved records, use dataRecord.custom.invoice instead, which preserves record identity. Both are supported.
In the app: choose Workflows → Invoice intake → Dev → New flow, create Process invoice, and declare its input and outputs under Inputs and Outputs. In the parent, choose Add action → Run flow on list, then configure Flow → Data type of items → List of items and map Invoice to the current item.
With an assistant: let the scaffold create Process invoice. The scaffold always creates a new body.
Scaffold from the exact list output
Section titled “Scaffold from the exact list output”Start by listing the workflows.
cai workflow list --limit 100 --jsonSelect Invoice intake from data.items, using its id as <workflowId>. If two entries share the name, ask the person which one. If data.hasMore is true, raise the CLI limit before concluding it is absent; MCP workflow_list caps limit at 100. This page pins the workflow once, so later CLI commands omit --workflow, while scoped MCP calls require workflowId.
Pin it, then read the outline:
cai use "<workflowId>" --jsoncai workflow outline --jsonFrom data.flows, note the parent and Store invoice IDs as <parentFlowId> and <storeFlowId>. Note the parent’s source node ID as <sourceNodeId>, then read that node’s outputs:
cai node get "<sourceNodeId>" --jsonUse the selected data.outputs[].id as <outputId>. For a whole output containing the invoice list:
cai flow for-each --flow "<parentFlowId>" --from "<sourceNodeId>" --source-output "<outputId>" --name "Process invoice" --item-type custom.invoice --item-name Invoice --max-items 500 --jsonMCP flow_for_each_scaffold uses sourceOutput, itemType, itemName, and maxItems; its source is either from plus sourceOutput, or listJs alone.
For a nested list, add --list-field "<path>" with the actual source field path. MCP listField also requires the source-output form. Note data.bodyFlowId as <bodyFlowId> and data.loopNodeId as <loopNodeId>. Inspect data.itemFlowInput, data.listWiring.listExpressionJs, the warnings, and nextSteps. A type mismatch only warns, so fix it before running.
The body, list wiring, and item mapping are created before the CLI sets an explicit cap. If that later step fails, the scaffold remains. Inspect the returned IDs and reread the outline before retrying.
Map the current item into the body
Section titled “Map the current item into the body”item exists in the parent’s per-item mapping. The body uses its own flow input binding. Data type of items reloads the list field, so after changing it, reread the props and recheck the list expression and mappings. The reload preserves existing expressions when the editor input type stays unchanged, so a saved expression can still target the old type.
In the app: use Go to flow, add Run flow, select Store invoice, and map its three inputs from Invoice.
Find the Run flow action before adding it:
cai integration actions "Run flow" --jsonMCP uses integration_action_list with query. Use Run flow’s data.items[].nodeSlug and key as <slug> and <componentKey>, then inspect it and add it to the body:
cai integration action --integration "<slug>" --key "<componentKey>" --jsoncai node add --flow "<bodyFlowId>" --node "<slug>" --key "<componentKey>" --name "Store this invoice" --jsonMCP uses integration_action_get with integration and key, then node_add with node for the slug. Note data.nodeId as <callNodeId>. Point that call at Store invoice, then read its mapping props and bindings:
cai node set-flow "<callNodeId>" --flow "<storeFlowId>" --jsoncai node props "<callNodeId>" --jsoncai expr context "<callNodeId>" --jsonMCP node_subflow_set uses flow for the target; node_prop_list reads mapping props and expr_context reads bindings.
Use the three returned mapping names as <invoiceIdProp>, <amountProp>, and <reviewNeededProp>. If context advertises flow.invoice, map:
cai expr set "<callNodeId>" --prop "<invoiceIdProp>" --js 'flow.invoice.invoice_id' --jsoncai expr set "<callNodeId>" --prop "<amountProp>" --js 'flow.invoice.amount' --jsoncai expr set "<callNodeId>" --prop "<reviewNeededProp>" --js 'flow.invoice.review_needed' --jsonMCP uses node_prop_set with js for each expression.
Editing a required child input, or an optional one that is already enabled, synchronizes ordinary callers. A new optional mapping has to be enabled. Reread the caller after contract edits. The node set-flow warning requiring relinking is stale.
Declare the body’s two outputs, then configure a complete Return response after the call, following Define Flow Contracts. An untouched text output writes empty text, and an untouched non-text output writes null. Later completed Returns overwrite earlier values.
Choose the cap and failure behavior
Section titled “Choose the cap and failure behavior”Max items defaults to 500 and accepts integers 1–10000. A list over the cap fails before any item runs, and it is not truncated. The whole run also shares 25,000 node executions, counting child nodes and scheduled skips. Calls beyond Max subflow depth fail, and that setting defaults to 100 with a range of 1–10000. If a child fails at either limit, iteration continuation records the failure and attempts later items.
In the app, leave Continue to next item if an iteration fails off to stop at a failed child. With an assistant, write the same setting:
cai node set "<loopNodeId>" --prop continueOnIterationError --value false --jsonMCP node_prop_set takes a boolean value. At scaffold creation, continuation is CLI --continue-on-error or MCP continueOnError.
Stopping loses the aggregate list. Earlier effects remain, and a new run starts at item zero and repeats them. Iteration continuation retains failed-child records and moves on, but it cannot rescue list-expression or pre-child mapping errors. Node-level Continue on error lets the parent continue after a failed loop, but never resumes remaining items.
A child finishing completed_with_error keeps its outputs and has a null iteration error. It never triggers the stop switch, but it counts toward the loop’s error message, such as 1 of 2 iterations failed. The loop and parent finish completed_with_error unless a later failure ends the parent.
Inspect every iteration
Section titled “Inspect every iteration”Downstream expressions receive the iteration list from .data. Execution inspection shows {data: [...], error: null|string}. Each record has index, outputs, status, and error. A failed record has null outputs and an error. An empty list returns [] without children.
In Dev, select the body, then Test. Use one safe invoice before testing the parent. Open Run history → Flow Executions → View to inspect children.
With an assistant, lay out both flows, validate the workflow, read both input sets, and run the body once:
cai flow layout "<bodyFlowId>" --jsoncai flow layout "<parentFlowId>" --jsoncai workflow validate --jsoncai run inputs "<bodyFlowId>" --flow --jsoncai run inputs "<parentFlowId>" --flow --jsoncai run flow "<bodyFlowId>" --target dev --inputs '{"Invoice":{"invoice_id":"INV-104","amount":120,"review_needed":false}}' --jsonMCP uses flow_layout, workflow_validate, run_inputs with nodeIdOrFlowId and flow: true, and run_flow with parsed inputs.
Confirm the advertised input labels match these fixtures. Run each separately and inspect its execution before the next:
cai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[]}' --jsoncai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[{"invoice_id":"INV-104","amount":120,"review_needed":false}]}' --jsoncai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[{"invoice_id":"INV-104","amount":120,"review_needed":false},{"invoice_id":"INV-105","amount":1500,"review_needed":true}]}' --jsonRun the two-item fixture again with the cap set to 1. No child should start. Restore 500 afterward.
cai node set "<loopNodeId>" --prop maxItems --value 1 --jsoncai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[{"invoice_id":"INV-104","amount":120,"review_needed":false},{"invoice_id":"INV-105","amount":1500,"review_needed":true}]}' --jsoncai node set "<loopNodeId>" --prop maxItems --value 500 --jsonTo test a failed child, supply null for Store invoice’s referenced, required Amount. Its numeric mapping stays missing, and the child fails input validation.
cai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[{"invoice_id":"INV-105","amount":null,"review_needed":true}]}' --jsonAfter each run, use data.workflowExecutionId as <workflowExecutionId>, or error.detail.workflowExecutionId on failure. A polling timeout does not cancel work, so inspect that execution.
cai exec tree "<workflowExecutionId>" --limit 200 --jsonThe tree defaults to 50 rows and caps at 200, and the rows omit outputs. While data.nextCursor exists, use it as <opaque> to read the next page:
cai exec tree "<workflowExecutionId>" --limit 200 --cursor "<opaque>" --jsonUse the loop row’s data.items[].id as <nodeExecutionId>:
cai exec node "<nodeExecutionId>" --jsonMCP uses execution_tree with workflowExecutionId and cursor, then execution_node_get with nodeExecutionId and full: true. CLI JSON already includes raw output, and --full expands human rendering. Recent execution lists default to 10 runs, and MCP caps them at 100. Read the iteration statuses and resolved mappings, then read back the stored records.
Call the same flow once
Section titled “Call the same flow once”For a separate one-off parent, choose New flow, add Run flow, select Store invoice, and map its inputs. The parent waits for the child and receives its named outputs.
Create that parent and note data.flowId as <oneOffFlowId>.
cai flow create --name "Store one invoice" --jsonReuse the discovered Run flow action pair:
cai node add --flow "<oneOffFlowId>" --node "<slug>" --key "<componentKey>" --name "Store one invoice" --jsonUse data.nodeId as <oneOffNodeId>, then read its mapping names:
cai node set-flow "<oneOffNodeId>" --flow "<storeFlowId>" --jsoncai node props "<oneOffNodeId>" --jsonUse the matching three mapping names for this call. These literal values need no item binding:
cai node set "<oneOffNodeId>" --prop "<invoiceIdProp>" --value INV-104 --jsoncai node set "<oneOffNodeId>" --prop "<amountProp>" --value 120 --jsoncai node set "<oneOffNodeId>" --prop "<reviewNeededProp>" --value false --jsoncai flow layout "<oneOffFlowId>" --jsoncai workflow validate --json