Skip to content

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:

FlowRequired inputsOutputsPurpose
Check invoiceInvoice ID: text; Amount: numberinvoice_id: text; review_needed: booleanFlag amounts over 1000.
Store invoiceInvoice ID: text; Amount: number; Review needed: booleaninvoice_id: text; status: textAccept the decision for later storage.

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.

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

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

Terminal window
cai use <workflowId> --json
cai flow list --json

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

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

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

Terminal window
cai flow get <flowId> --json
cai flow get <storeFlowId> --json

Run only the missing declarations. An input is required unless you pass --optional.

Terminal window
cai flow input add <flowId> --display "Invoice ID" --type text --json
cai flow input add <flowId> --display "Amount" --type number --json
cai flow input add <storeFlowId> --display "Invoice ID" --type text --json
cai flow input add <storeFlowId> --display "Amount" --type number --json
cai flow input add <storeFlowId> --display "Review needed" --type boolean --json

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

CallerMissing-value behavior
External workflow APIRejects 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 subflowValidates 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 testUses supplied values or saved test values for referenced inputs; absent text becomes "", other absent types become null.
Isolated-node testRequires every referenced flow-input ID explicitly; omission fails with ISOLATED_FLOW_CONTEXT_REQUIRED. Explicit null, "", false, and zero count as supplied.

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.

Terminal window
cai flow output add <flowId> --display "Invoice ID" --name invoice_id --type text --json
cai flow output add <flowId> --display "Review needed" --name review_needed --type boolean --json
cai flow output add <storeFlowId> --display "Invoice ID" --name invoice_id --type text --json
cai flow output add <storeFlowId> --display "Status" --name status --type text --json
cai integration actions "Return response" --json

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

Terminal window
cai integration action --integration <slug> --key <componentKey> --json
cai node add --flow <flowId> --node <slug> --key <componentKey> --name "Return decision" --json

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

Terminal window
cai expr context <nodeId> --prop invoice_id --json
cai expr context <nodeId> --prop review_needed --json

Use the current data.flow[].js names. If they are flow.invoiceId and flow.amount, set:

Terminal window
cai expr set <nodeId> --prop invoice_id --js 'flow.invoiceId' --json
cai expr set <nodeId> --prop review_needed --js 'flow.amount > 1000' --json

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

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:

Terminal window
cai flow input update <flowId> --input <inputId> --display "Invoice amount" --json
cai expr context <nodeId> --prop review_needed --json
cai expr decompile <nodeId> --prop review_needed --json

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

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.

Terminal window
cai flow layout <flowId> --json
cai workflow validate --json
cai run inputs <flowId> --flow --json

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

Terminal window
cai run flow <flowId> --target dev --inputs '{"<invoiceInputId>":"INV-104","<amountInputId>":120}' --json
cai run flow <flowId> --target dev --inputs '{"<invoiceInputId>":"INV-105","<amountInputId>":1500}' --json

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