Skip to content

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.

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.

Start by listing the workflows.

Terminal window
cai workflow list --limit 100 --json

Select 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:

Terminal window
cai use "<workflowId>" --json
cai workflow outline --json

From 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:

Terminal window
cai node get "<sourceNodeId>" --json

Use the selected data.outputs[].id as <outputId>. For a whole output containing the invoice list:

Terminal window
cai flow for-each --flow "<parentFlowId>" --from "<sourceNodeId>" --source-output "<outputId>" --name "Process invoice" --item-type custom.invoice --item-name Invoice --max-items 500 --json

MCP 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.

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:

Terminal window
cai integration actions "Run flow" --json

MCP 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:

Terminal window
cai integration action --integration "<slug>" --key "<componentKey>" --json
cai node add --flow "<bodyFlowId>" --node "<slug>" --key "<componentKey>" --name "Store this invoice" --json

MCP 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:

Terminal window
cai node set-flow "<callNodeId>" --flow "<storeFlowId>" --json
cai node props "<callNodeId>" --json
cai expr context "<callNodeId>" --json

MCP 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:

Terminal window
cai expr set "<callNodeId>" --prop "<invoiceIdProp>" --js 'flow.invoice.invoice_id' --json
cai expr set "<callNodeId>" --prop "<amountProp>" --js 'flow.invoice.amount' --json
cai expr set "<callNodeId>" --prop "<reviewNeededProp>" --js 'flow.invoice.review_needed' --json

MCP 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.

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:

Terminal window
cai node set "<loopNodeId>" --prop continueOnIterationError --value false --json

MCP 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.

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:

Terminal window
cai flow layout "<bodyFlowId>" --json
cai flow layout "<parentFlowId>" --json
cai workflow validate --json
cai run inputs "<bodyFlowId>" --flow --json
cai run inputs "<parentFlowId>" --flow --json
cai run flow "<bodyFlowId>" --target dev --inputs '{"Invoice":{"invoice_id":"INV-104","amount":120,"review_needed":false}}' --json

MCP 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:

Terminal window
cai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[]}' --json
cai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[{"invoice_id":"INV-104","amount":120,"review_needed":false}]}' --json
cai 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}]}' --json

Run the two-item fixture again with the cap set to 1. No child should start. Restore 500 afterward.

Terminal window
cai node set "<loopNodeId>" --prop maxItems --value 1 --json
cai 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}]}' --json
cai node set "<loopNodeId>" --prop maxItems --value 500 --json

To test a failed child, supply null for Store invoice’s referenced, required Amount. Its numeric mapping stays missing, and the child fails input validation.

Terminal window
cai run flow "<parentFlowId>" --target dev --inputs '{"Invoices":[{"invoice_id":"INV-105","amount":null,"review_needed":true}]}' --json

After each run, use data.workflowExecutionId as <workflowExecutionId>, or error.detail.workflowExecutionId on failure. A polling timeout does not cancel work, so inspect that execution.

Terminal window
cai exec tree "<workflowExecutionId>" --limit 200 --json

The 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:

Terminal window
cai exec tree "<workflowExecutionId>" --limit 200 --cursor "<opaque>" --json

Use the loop row’s data.items[].id as <nodeExecutionId>:

Terminal window
cai exec node "<nodeExecutionId>" --json

MCP 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.

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>.

Terminal window
cai flow create --name "Store one invoice" --json

Reuse the discovered Run flow action pair:

Terminal window
cai node add --flow "<oneOffFlowId>" --node "<slug>" --key "<componentKey>" --name "Store one invoice" --json

Use data.nodeId as <oneOffNodeId>, then read its mapping names:

Terminal window
cai node set-flow "<oneOffNodeId>" --flow "<storeFlowId>" --json
cai node props "<oneOffNodeId>" --json

Use the matching three mapping names for this call. These literal values need no item binding:

Terminal window
cai node set "<oneOffNodeId>" --prop "<invoiceIdProp>" --value INV-104 --json
cai node set "<oneOffNodeId>" --prop "<amountProp>" --value 120 --json
cai node set "<oneOffNodeId>" --prop "<reviewNeededProp>" --value false --json
cai flow layout "<oneOffFlowId>" --json
cai workflow validate --json