Define Flow Contracts
How do you make a flow reusable? Define its inputs and types, declare its outputs, and map them in Return response. If no Return completes, a successful flow returns {}.
These example contracts belong to Invoice intake, in the development version:
| Flow | Required inputs | Outputs | Purpose |
|---|---|---|---|
| Check invoice | Invoice ID: text; Amount: number | invoice_id: text; review_needed: boolean | Flag amounts over 1000. |
| Store invoice | Invoice ID: text; Amount: number; Review needed: boolean | invoice_id: text; status: text | Accept the decision for later storage. |
Define what callers supply
Section titled “Define what callers supply”In the app: open Workflows → Invoice intake → Dev. Create a missing flow with New flow. Then open Inputs → Add new input and set Display name, type, and Required.
With an assistant: this page pins the workflow once with cai use, so later commands omit --workflow. MCP calls always supply workflowId. Start with the workflow list.
cai workflow list --limit 100 --jsonTake Invoice intake’s data.items[].id as <workflowId>. If several match, ask which one. If data.hasMore is true, raise the CLI limit or copy the exact ID from the app. The MCP limit caps at 100.
Pin the workflow, then list its flows.
cai use <workflowId> --jsoncai flow list --jsonUse the data.items[].id of each existing flow. Create the missing ones, and note each data.flowId: Check invoice as <flowId>, Store invoice as <storeFlowId>.
cai flow create --name "Check invoice" --jsoncai flow create --name "Store invoice" --jsonRead both contracts before adding fields. A label that already exists creates a second input. Compare the data.flowInputs IDs, labels, types, and optional values, and look at data.flowOutputs.
cai flow get <flowId> --jsoncai flow get <storeFlowId> --jsonRun only the missing declarations. An input is required unless you pass --optional.
cai flow input add <flowId> --display "Invoice ID" --type text --jsoncai flow input add <flowId> --display "Amount" --type number --jsoncai flow input add <storeFlowId> --display "Invoice ID" --type text --jsoncai flow input add <storeFlowId> --display "Amount" --type number --jsoncai flow input add <storeFlowId> --display "Review needed" --type boolean --jsonMCP uses flow_input_add with flowId, display, type, and optional: false.
A required input still accepts a blank invoice ID or a negative amount. Use If / Else to route invalid invoices to an explicit invalid-input result. Only when skips just its node.
| Caller | Missing-value behavior |
|---|---|
| External workflow API | Rejects any missing declared required input before creating an execution. flow_inputs accepts IDs, stored names, or display names; unknown keys and conflicting aliases are rejected. |
| Trigger or subflow | Validates referenced inputs only: missing required values fail the frame; omitted optional values become null; unused inputs are ignored. Stored default metadata does not fill them. |
| Development root-flow test | Uses supplied values or saved test values for referenced inputs; absent text becomes "", other absent types become null. |
| Isolated-node test | Requires every referenced flow-input ID explicitly; omission fails with ISOLATED_FLOW_CONTEXT_REQUIRED. Explicit null, "", false, and zero count as supplied. |
Return the complete decision
Section titled “Return the complete decision”In the app: add each output under Outputs → Add new output. Then choose Add action → Actions, search Return response, and configure every field.
With an assistant: add only the missing outputs, each with an explicit caller-facing key. The last command finds Return response.
cai flow output add <flowId> --display "Invoice ID" --name invoice_id --type text --jsoncai flow output add <flowId> --display "Review needed" --name review_needed --type boolean --jsoncai flow output add <storeFlowId> --display "Invoice ID" --name invoice_id --type text --jsoncai flow output add <storeFlowId> --display "Status" --name status --type text --jsoncai integration actions "Return response" --jsonNote Return response’s data.items[].nodeSlug as <slug> and its paired key as <componentKey>. Reuse a Return that already exists. Otherwise inspect the discovered action and add it.
cai integration action --integration <slug> --key <componentKey> --jsoncai node add --flow <flowId> --node <slug> --key <componentKey> --name "Return decision" --jsonMCP uses integration_action_list for discovery and integration_action_get with integration and key for inspection.
Note the new data.nodeId as <nodeId>. For an existing Return, use its data.nodes[].id from flow get. Read each Return field’s bindings.
cai expr context <nodeId> --prop invoice_id --jsoncai expr context <nodeId> --prop review_needed --jsonUse the current data.flow[].js names. If they are flow.invoiceId and flow.amount, set:
cai expr set <nodeId> --prop invoice_id --js 'flow.invoiceId' --jsoncai expr set <nodeId> --prop review_needed --js 'flow.amount > 1000' --jsonMCP’s expression setter is node_prop_set with nodeId, prop, and js.
Wire producers directly into the Return when a field reads their output. This example reads flow inputs. Fill Store invoice’s Return when you implement storage.
A completed Return writes every output, not only the ones you set. Untouched text writes "", and untouched non-text writes null. Later completed Returns overwrite earlier values. Use one complete Return per execution path.
Output edits synchronize every Return automatically, despite the stale flow output add warning. A rename rekeys the field. A type change that changes the editor resets values and expressions. Reread each Return after a contract edit.
Rename without replacing identity
Section titled “Rename without replacing identity”An input’s ID, stored name, display label, and current expression binding differ. A label change recomputes the bindings, while stored expressions keep their IDs. An output’s name is the caller’s key; retain it during a display rename with flow output update --name.
In the app: edit Amount’s Display name under Inputs. With an assistant: take Amount’s data.flowInputs[].id from flow get as <inputId>, rename it, and read the binding back:
cai flow input update <flowId> --input <inputId> --display "Invoice amount" --jsoncai expr context <nodeId> --prop review_needed --jsoncai expr decompile <nodeId> --prop review_needed --jsonMCP uses flow_input_update with input for the input ID and display for its label.
Deleting and recreating an input removes the old caller mappings and leaves dangling expressions. Validation reports DANGLING_FLOW_INPUT_REFERENCE, and evaluation fails. Rename in place, or repair the dependents.
Check what callers receive
Section titled “Check what callers receive”In the app: select Dev → Check invoice → Test, then inspect Run history. These tests only decide, so they store no invoices.
With an assistant: lay out the flow, validate it, and read its inputs.
cai flow layout <flowId> --jsoncai workflow validate --jsoncai run inputs <flowId> --flow --jsonFrom the returned data.flowInputs[].id values, use Invoice ID as <invoiceInputId> and Invoice amount as <amountInputId>.
Run the flow below and above the review threshold.
cai run flow <flowId> --target dev --inputs '{"<invoiceInputId>":"INV-104","<amountInputId>":120}' --jsoncai run flow <flowId> --target dev --inputs '{"<invoiceInputId>":"INV-105","<amountInputId>":1500}' --jsonMCP uses run_inputs with nodeIdOrFlowId and flow: true, then run_flow with parsed inputs and target: "dev".
Check each data.output. Expect the matching invoice_id, with review_needed false then true. A Run flow caller receives these named outputs. Tab order does not call Store invoice. Continue with Loop and Reuse Flows.