@controllerai/cli 0.2.17 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/api/client.js +32 -0
  2. package/dist/api/client.js.map +1 -1
  3. package/dist/commands/agent.js +101 -8
  4. package/dist/commands/agent.js.map +1 -1
  5. package/dist/commands/expr.js +3 -10
  6. package/dist/commands/expr.js.map +1 -1
  7. package/dist/commands/file.js +11 -1
  8. package/dist/commands/file.js.map +1 -1
  9. package/dist/commands/integration.js +8 -2
  10. package/dist/commands/integration.js.map +1 -1
  11. package/dist/commands/workflow.js +29 -6
  12. package/dist/commands/workflow.js.map +1 -1
  13. package/dist/errors.js +5 -2
  14. package/dist/errors.js.map +1 -1
  15. package/package.json +1 -1
  16. package/skills/controllerai-agents/SKILL.md +25 -192
  17. package/skills/controllerai-agents/references/command-reference.md +205 -0
  18. package/skills/controllerai-ai-nodes/SKILL.md +11 -92
  19. package/skills/controllerai-ai-nodes/references/command-reference.md +98 -0
  20. package/skills/controllerai-data/SKILL.md +34 -159
  21. package/skills/controllerai-data/references/command-reference.md +177 -0
  22. package/skills/controllerai-expressions/SKILL.md +29 -151
  23. package/skills/controllerai-expressions/references/command-reference.md +166 -0
  24. package/skills/controllerai-expressions/references/dsl-and-recipes.md +1 -1
  25. package/skills/controllerai-flow-control/SKILL.md +21 -160
  26. package/skills/controllerai-flow-control/references/command-reference.md +171 -0
  27. package/skills/controllerai-integration-actions/SKILL.md +11 -112
  28. package/skills/controllerai-integration-actions/references/command-reference.md +118 -0
  29. package/skills/controllerai-node-config/SKILL.md +18 -178
  30. package/skills/controllerai-node-config/references/command-reference.md +186 -0
  31. package/skills/controllerai-runs/SKILL.md +25 -117
  32. package/skills/controllerai-runs/references/command-reference.md +113 -0
  33. package/skills/controllerai-start/SKILL.md +36 -67
  34. package/skills/controllerai-start/references/mcp-tool-map.md +6 -0
  35. package/skills/controllerai-triggers/SKILL.md +16 -210
  36. package/skills/controllerai-triggers/references/command-reference.md +215 -0
  37. package/skills/controllerai-workflows/SKILL.md +24 -104
  38. package/skills/controllerai-workflows/references/command-reference.md +118 -0
  39. package/skills/manifest.json +120 -60
@@ -0,0 +1,166 @@
1
+ # Controller AI expressions
2
+
3
+
4
+ Controller AI uses a restricted JavaScript-like authoring language that is compiled into the workflow engine's expression AST. It is not general JavaScript. Discover the real scope and compile before every write.
5
+
6
+
7
+ - Wire first; refresh context after structural changes. See Required write loop.
8
+ - `--value` is static; dynamic values use `--js`. See Typed values and templates.
9
+ - Copy current bindings, not labels or stale names. See Read the destination's exact scope.
10
+ - Never use raw ASTs, general JavaScript, or private APIs. See Use only advertised syntax.
11
+ - Keep ternaries in the destination type. See Typed values and templates.
12
+ - Prove resolution in a containing-flow run. See Verify runtime resolution.
13
+
14
+ Data types are the TypeScript types of workflows: every expression is typed, and an expression can only be written against a known type. Integration types describe what integration actions output (resolved by a successful dev run); `custom.*` types describe shapes for data-store records and AI-node structured outputs. Two wrappers matter: `dataRecord.custom.x` means "an actual stored record of that shape" — the fields plus record identity (`id`, `createdAt`) — while bare `custom.x` is just data in that shape; `list.` prefixes a list of any type. When an expression cannot be authored, the cause is usually a missing type — resolve the upstream node's output type first, don't guess field paths.
15
+
16
+ `.every()` accepts the same multi-condition predicates as `.filter()` and compiles to `:all match`. A removed schema field makes `.every()` false but is dropped by `.filter()`. Comparator semantics govern missing, null, and unparseable values; guards must require presence explicitly.
17
+
18
+ ## How empty values evaluate
19
+
20
+ An empty input evaluates to its typed empty form: `""` for TEXT, null/undefined for objects. Both cases below fail silently:
21
+
22
+ - **A filter compared with empty matches no records.** To skip an omitted constraint, opt into `ignoreEmptyConstraints`; see [dsl-and-recipes.md](dsl-and-recipes.md).
23
+ - **An omitted optional TEXT input arrives as `""`.** Equality then matches nothing and can make a create-if-missing step duplicate data.
24
+
25
+ When a run unexpectedly returns zero results, check the resolved configs for empty comparison values first (`cai exec node <nodeExecutionId> --full`).
26
+
27
+ ## Required write loop
28
+
29
+ ### 1. Wire first
30
+
31
+ An upstream output is available only after it is connected to the target node. If context lists it under `unwiredUpstreamOutputs`, connect it, then fetch context again.
32
+
33
+ ```bash
34
+ cai node connect --flow <flowId> --from <sourceNodeId> --to <targetNodeId> --json
35
+ ```
36
+
37
+ Use `cai branch connect` for Router or If/Else outputs.
38
+
39
+ ### 2. Read the destination's exact scope
40
+
41
+ ```bash
42
+ cai expr context <nodeId> --prop <propName> --json
43
+ ```
44
+
45
+ Copy binding and field names exactly. Important sources include:
46
+
47
+ - `flow.<name>`: flow input
48
+ - `inputs.<name>`: wired node input; the payload is usually already unwrapped
49
+ - `item`: injected current item, only where context exposes it
50
+ - `workflowData("custom.type")`: records from an active workflow collection
51
+ - `now`, `isLiveVersion`: constants shown by context
52
+
53
+ Do not derive a binding from a display label. Context flags duplicate display names and supplies the de-duplicated JS name, input ID, and source node.
54
+
55
+ When compilation reports an unknown field, copy a typeable display-derived accessor it advertises; field IDs appear second for disambiguation.
56
+
57
+ ### 3. Use only advertised syntax
58
+
59
+ Get the current operation catalog when needed:
60
+
61
+ ```bash
62
+ cai expr ops --workflow <workflowId> --type <text|number|date|list|boolean|file|custom> --json
63
+ ```
64
+
65
+ The catalog is workflow-scoped because it is read from expression context. On CLI, an existing `cai use` or `CAI_WORKFLOW` selection can replace the explicit flag; MCP `expr_operation_list` always requires `workflowId`.
66
+
67
+ Keep the program to optional `const` declarations followed by one final expression. Do not submit raw AST JSON or probe private APIs. Do not use loops, assignments, `new`, regular expressions, optional chaining, arbitrary library methods, or object construction unless the compiler catalog supports them.
68
+
69
+ Date values are ISO-8601 strings on the wire (`"2026-06-05"` or `"2026-06-05T12:30:00Z"`). A `date`-typed flow input or field is used directly with date operations; only `text` values need `.toDate()` first. For epoch text, pass the unit literal explicitly: `flow.timestamp.toDate("epoch_seconds")` or `flow.timestamp.toDate("epoch_millis")`. The ops table renders that argument generically; never guess the unit from digit count.
70
+
71
+ #### Typed values and templates
72
+
73
+ `--value` stores a literal, so `flow.name`, `{inputs.Name}`, `{{name}}`, and `${name}` stay static. `--js` and `--js-file` compile typed expressions. `${...}` interpolates only inside a backtick template expression, which returns text. Keep non-text values as direct typed expressions.
74
+
75
+ Ternaries preserve compatible scalar branch types: number and boolean branches stay number and boolean. A different inferred destination type means the write is incomplete. Report the compiler error; do not quote or coerce branches to text.
76
+
77
+ Read [dsl-and-recipes.md](dsl-and-recipes.md) for canonical patterns.
78
+
79
+ ### 4. Compile, write, and read back
80
+
81
+ For a node prop, prefer:
82
+
83
+ ```bash
84
+ cai expr set <nodeId> --prop <propName> --js '<expression>' --json
85
+ ```
86
+
87
+ MCP: use `node_prop_set` with `js`; it merges `cai expr set` with `cai node set`.
88
+
89
+ For multiline expressions or shell-sensitive templates on CLI, put the exact source in a UTF-8 file and use `--js-file <path>`. Use `--js-file -` to read stdin. It is mutually exclusive with inline `--js` and is available on `expr validate/set`, `node set/only-when`, and `branch add/update`.
90
+
91
+ ```bash
92
+ cai expr set <nodeId> --prop <propName> --js-file expression.js --json
93
+ ```
94
+
95
+ MCP: pass the file contents inline as `js`; the hosted process does not read the local path or stdin.
96
+
97
+ This fetches target metadata, auto-enables a catalog-only optional target when needed, compiles before writing the value, persists to the correct channel, and verifies read-back. If validation or the value write then fails, the prop stays enabled; repair with `cai node disable-prop <nodeId> --prop <propName>`.
98
+
99
+ MCP: repair with `node_optional_prop_set` and `enabled: false`.
100
+
101
+ Array channels are not interchangeable. On a value-channel `SHORT_TEXT_ARRAY`, a scalar expression replaces the whole array with one dynamic element; it does not append. List-typed expressions on value-channel array props are rejected. Use `cai node set <nodeId> --prop <propName> --value '["a","b"]'` for a static list, or use a list-typed expression only where prop metadata exposes whole-array expression mode.
102
+
103
+ For a side-effect-free check:
104
+
105
+ ```bash
106
+ cai expr validate <nodeId> --prop <propName> --js '<expression>' --json
107
+ ```
108
+
109
+ For conditions:
110
+
111
+ ```bash
112
+ cai expr validate <nodeId> --mode onlyWhen --js '<boolean expression>' --json
113
+ cai node only-when <nodeId> --js '<boolean expression>' --json
114
+ ```
115
+
116
+ Node `only-when` takes one restricted boolean through `--js` or `--js-file`, or grouped conditions through `--groups-json '[["a","b"],["c"]]'` (inner arrays are AND, outer rows are OR). Root scalar `&&` or `||` inside one expression is unsupported; use `--groups-json` for compound guards. Branch add/update accepts `string[][]`: outer rows are OR, inner entries are AND, and each entry compiles separately.
117
+
118
+ ```bash
119
+ cai branch update <nodeId> --route <routeId> --condition-json '[["flow.email.isNotEmpty()","flow.units < 5"],["flow.override === true"]]' --json
120
+ ```
121
+
122
+ Use grouping only when branch semantics fit. For a compound node-only guard, use `--groups-json`; do not weaken the rule or probe private APIs. To make missing numeric input fail `< 5`, use `(flow.unitsLeft ?? 5) < 5`.
123
+
124
+ Router and If/Else conditions are written with `cai branch add` or `cai branch update` after the same context/validation discipline.
125
+
126
+ ### 5. Verify runtime resolution
127
+
128
+ ```bash
129
+ cai expr decompile <nodeId> --prop <propName> --json
130
+ cai run node <nodeId> --inputs <values.json> --json
131
+ ```
132
+
133
+ MCP: `run_node` takes the parsed `inputs` object and `waitSeconds` (default 60, max 120).
134
+
135
+ Use `run node` to isolate the destination's own configuration. It never executes wired upstream nodes. If the expression consumes `inputs.<name>`, also run its containing flow:
136
+
137
+ ```bash
138
+ cai run flow <flowId> --inputs <values.json> --json
139
+ ```
140
+
141
+ MCP: `run_flow` takes the parsed `inputs` object and `waitSeconds` (default 60, max 120).
142
+
143
+ Inspect `runScope` and `resolvedConfigs` in the run result. A null or missing dynamic config is an expression failure, not success; a partially empty value is equally suspect. Only the full flow run proves an upstream-derived value. Use `cai exec node <nodeExecutionId> --full --json` to inspect the final status, resolved configs, and untruncated output.
144
+
145
+ ## Hard rules
146
+
147
+ - Never guess a binding, field, method, or target type.
148
+ - Never author against an unwired output.
149
+ - Never add `.data` automatically. Use the exact path shown by context.
150
+ - Re-read context after rewiring, renaming inputs, changing schemas, changing dynamic selectors, or resolving an output type.
151
+ - Preserve `0` and `false`; they are values, not missing data.
152
+ - Optional filters need an explicit behavior decision. Do not silently change fail-closed filtering to match-all.
153
+ - A compiler success proves syntax and types, not correct source selection. Verify the source node/input IDs when names are ambiguous and inspect runtime values.
154
+
155
+ ## Before you finish
156
+
157
+ ```bash
158
+ cai expr context <nodeId> --prop <propName> --json
159
+ cai expr validate <nodeId> --prop <propName> --js '<expression>' --json
160
+ cai expr decompile <nodeId> --prop <propName> --json
161
+ cai run flow <flowId> --inputs <values.json> --json
162
+ ```
163
+
164
+ - Context must show current source IDs and bindings.
165
+ - Validation and decompile must agree on source and type.
166
+ - The flow run must show the intended value in `runScope` and `resolvedConfigs`.
@@ -90,7 +90,7 @@ workflowData("custom.lead").filter(
90
90
  )
91
91
  ```
92
92
 
93
- Test both the supplied and omitted cases. Empty text may behave as a real value rather than a missing value; do not assume the opt-in drops it.
93
+ Test both supplied and omitted cases. With this option, null, omitted values, empty text and empty lists are skipped; zero and false remain real constraints. Optional OR filters preserve input order and duplicates, and return all rows when every branch is skipped. Use `.take(count)` and `.skip(offset)` for pagination.
94
94
 
95
95
  ## Loop item
96
96
 
@@ -1,178 +1,39 @@
1
1
  ---
2
2
  name: controllerai-flow-control
3
- description: Design and configure Controller AI execution paths using only-when rules, Router routes, If/Else branches, for-each loops, and subflows. Use when work must branch, fan out, skip, iterate over a list, call another flow, converge paths, or handle errors without changing the user's intended semantics.
3
+ description: Build Controller AI execution paths with conditions, routers, if/else branches, loops, joins and subflow calls. Use when a flow must validate, branch, iterate or reuse another flow.
4
4
  ---
5
5
 
6
- # Controller AI flow control
6
+ # Routing, loops and reusable flows
7
7
 
8
- The `cai ...` examples are canonical grammar. Use the transport selected in `controllerai-start` and its MCP tool map; never mix transports in one task.
8
+ Control-flow nodes decide which steps execute and how data moves between paths. Use conditions to validate requests, a router for named alternatives, a loop for each item in a list, and subflows for reusable behavior.
9
9
 
10
- Choose by intended runtime semantics.
10
+ ## Choose the construct
11
11
 
12
- ## Rules that break tasks when skipped
12
+ - **Only when:** skip one node unless a condition holds.
13
+ - **If/Else:** two paths, such as record found versus not found.
14
+ - **Router:** several named routes, such as search modes or operation types.
15
+ - **For each / Run Flow on List:** execute a body flow for each typed item and collect iteration results.
16
+ - **Subflow:** call a reusable typed flow and map its inputs/outputs.
17
+ - **Join paths:** deliberately reconcile active paths before a shared continuation.
13
18
 
14
- - Guard the exact action with node `only-when`; see Conditions.
15
- - Use If/Else for one path and Router for all matches; see Pick the correct primitive.
16
- - Keep downstream wiring after node-only skip; see Conditions.
17
- - Never invent a missing condition or Router order; see If/Else and Router.
18
- - Build standard loops only with `flow for-each`; see Loops.
19
- - Converge paths through Join Paths; see Subflows and convergence.
20
- - Fail missing comparisons closed; keep money paths blocking. See Conditions, Loops, and the error pattern.
21
- - Never acknowledge validation errors without explicit approval; see `controllerai-workflows` Lifecycle rules.
19
+ Choose whether multiple matching routes should run or one alternative should win. Make invalid/missing-input paths return a clear result without performing the write. A skipped node is expected only if its condition and port explain why.
22
20
 
23
- ## Pick the correct primitive
21
+ ## Build branches
24
22
 
25
- | Intent | Primitive | Semantics |
26
- | --- | --- | --- |
27
- | Skip one action when a condition is false | Node `only-when` | No alternate path |
28
- | Choose exactly one path | If/Else | First matching branch runs; Else is last |
29
- | Run every matching path | Router | Conditions are independent; multiple routes may run |
30
- | Apply work to every list item | `flow for-each` | Atomic loop scaffold with typed item flow |
31
- | Reuse a typed unit of work once | Run flow/subflow node | Explicit input and output contract |
32
- | Continue after an action error | Error output or continue-on-error | Error behavior is explicit and testable |
23
+ Inspect the router's current expression context, define a route condition, and connect the route output to its path. Conditions can use the flow's inputs and wired upstream data. Use the supported structured OR-of-AND condition groups for compound logic; do not assume ordinary JavaScript syntax is valid in every condition surface.
33
24
 
34
- Do not substitute one primitive merely because its component is easier to find.
25
+ A condition accepting a record requires a fetched record, not a fabricated object. Test both branches, including false/zero values and a missing record. Declare and populate the flow output on each intended return path.
35
26
 
36
- ## Conditions
27
+ ## Build loops
37
28
 
38
- Before writing any condition:
29
+ Use the available for-each scaffolding operation to create the loop, body flow, typed item input and mappings together. It reduces the mistakes involved in assembling loop injection manually. Supply the list expression and exact source output when wiring an upstream list.
39
30
 
40
- ```bash
41
- cai expr context <controlNodeId> --json
42
- cai expr validate <controlNodeId> --mode onlyWhen --js '<boolean expression>' --json
43
- ```
31
+ The body receives one item; it does not receive the whole original list implicitly. Custom-shaped items and real data-record items have different types. Loop output represents iteration results, so inspect its actual fields before mapping a final list.
44
32
 
45
- For one guarded action:
33
+ For an uploaded CSV, a conversational agent can read rows and call a per-row flow. Use a native loop when the workflow already receives a typed list and deterministic iteration is desired.
46
34
 
47
- ```bash
48
- cai node only-when <nodeId> --js '<boolean expression>' --json
49
- ```
35
+ ## Verify paths
50
36
 
51
- Use `--clear` only when the user's intent is to remove the guard.
37
+ Run one representative case per route and inspect node status, selected output ports, skipped reasons and final outputs. For writes, verify the same-environment record effects. A successful outer loop does not prove every iteration succeeded; surface row failures in the result.
52
38
 
53
- Guard the action that must skip. Do not substitute If/Else for a node-only skip. Its `only_when_false` does not block ordinary output; downstream remains eligible. Keep wiring. Guard downstream separately or branch if it must also skip.
54
-
55
- Node `only-when` takes one restricted boolean through `--js` or `--js-file`, or grouped conditions through `--groups-json '[["a","b"],["c"]]'` (inner arrays are AND, outer rows are OR). Root scalar `&&` or `||` inside one expression is unsupported; use `--groups-json` for compound guards. Grouped `string[][]` conditions exist only on branch add/update, with outer rows OR and inner entries AND. Use them only when branch semantics fit. If an exact compound node-only guard is required, report it as unsupported; do not weaken the rule or probe private APIs. For a missing number that must fail `< 5`, use a same-type boundary such as `(flow.unitsLeft ?? 5) < 5`.
56
-
57
- Choose the primitive by the runtime signal that callers and tests need. A false node `only-when` records `skipReason: "only_when_false"`; a node on an untaken If/Else branch or unmatched Router route records `skipReason: "upstream_skipped"`.
58
-
59
- ## If/Else and Router
60
-
61
- Discover and initialize the appropriate control component using `controllerai-node-config`, then inspect its exact branch IDs:
62
-
63
- ```bash
64
- cai branch list <controlNodeId> --json
65
- ```
66
-
67
- A fresh If/Else already contains one blank `If` branch and an Else branch. The blank condition is a publish/runtime blocker: copy that default branch ID from `branch list` and update it before adding another condition branch. Do not leave the default blank and create a replacement beside it.
68
-
69
- Never invent a missing condition from node names, current values, or one observed run. If no authoritative platform surface supplies the business rule, leave the branch blank, report the blocker, and ask the user.
70
-
71
- ```bash
72
- cai branch update <ifElseNodeId> \
73
- --route <defaultIfBranchId> \
74
- --name "<meaning>" \
75
- --js '<boolean expression>' \
76
- --json
77
- ```
78
-
79
- Add another condition only when the decision needs another non-Else case. Router routes are independent, so update an existing blank route or add each intended route explicitly:
80
-
81
- ```bash
82
- cai branch add <controlNodeId> --name "<meaning>" --js '<boolean expression>' --json
83
- cai branch update <controlNodeId> --route <routeOrBranchId> --js '<boolean expression>' --json
84
- cai branch connect <controlNodeId> --route <routeOrBranchId> --to <targetNodeId> --flow <flowId> --json
85
- ```
86
-
87
- For If/Else, order is behavior:
88
-
89
- ```bash
90
- cai branch reorder <ifElseNodeId> --order <conditionId1,conditionId2> --json
91
- ```
92
-
93
- List every condition branch exactly once and omit Else; the operation keeps Else last. Router routes cannot be reordered because every match runs. Do not simulate order by deleting a route or serializing its downstream nodes. Leave the fan-out unchanged unless the user explicitly authorizes a redesign.
94
-
95
- After edits, run `cai branch list` again. Treat its warnings about missing conditions or unwired paths as blockers: an unwired selected path can end with an apparently green execution.
96
-
97
- ## Loops
98
-
99
- Never assemble the standard per-item subflow pattern by hand:
100
-
101
- ```bash
102
- cai node get <listProducingNodeId> --json
103
- cai flow for-each \
104
- --flow <parentFlowId> \
105
- --from <listProducingNodeId> \
106
- --source-output <exactOutputId> \
107
- --name "<item flow name>" \
108
- --item-type <text|number|date|boolean|custom.type> \
109
- --json
110
- ```
111
-
112
- Use repeatable `--item-field <id:type[:display]>` only when creating a new `custom.*` item type.
113
-
114
- Copy `--source-output` from the selected node's exact `outputs[].id`. It is an opaque ID, not `displayName`, and there is no first-output default. This matters for Router, If/Else, and error outputs as well as ordinary action nodes.
115
-
116
- - If the source's whole output is the list, omit both `--list-field` and `--list-js`.
117
- - If the list is nested in the source payload, use `--list-field` with the dot-delimited path below `.data`; the operation derives the not-yet-created loop input binding deterministically.
118
- - Use `--list-js` only for an advanced exact expression. For multiline or shell-sensitive source, use `--js-file <path|->` instead. Both are mutually exclusive with `--list-field` and with each other; copy the wrapper/binding shape from expression context rather than guessing it.
119
-
120
- MCP: pass expression source inline as `listJs` to `flow_for_each_scaffold`; the server cannot read a local `--js-file` path.
121
-
122
- - The runtime cap defaults to 500 items and accepts 1 through 10000. Set `--max-items` explicitly when the product requirement differs. The scaffold operation creates the core graph first, then materializes and verifies this optional prop. The cap is all-or-nothing at runtime: a list larger than the cap throws and NOTHING iterates — there is no partial truncation.
123
-
124
- Changing an existing loop: update its loop-node props directly:
125
-
126
- ```bash
127
- cai node set <loopNodeId> --prop maxItems --value <n> --json
128
- cai node set <loopNodeId> --prop continueOnIterationError --value true --json
129
- ```
130
-
131
- MCP: use `node_prop_set` for both writes.
132
-
133
- This is not `cai node continue-on-error`, which configures the node-level Error output — a different feature.
134
-
135
- Read every returned warning. A list/item type mismatch means the scaffold is not verified even though it was created. Follow the returned `nextSteps` in order.
136
-
137
- Inside loop mappings, `item` exists only where context exposes it. Inside the body flow, use its `flow.<itemInput>` binding. Downstream loop results are iteration records whose declared child outputs are under `.outputs`.
138
-
139
- Loops run sequentially. `--continue-on-error` applies to a failed child iteration; a list expression or mapping/config expression that fails before the child starts still aborts the loop. Test empty, one-item, several-item, over-limit, and intended iteration-error behavior.
140
-
141
- Never enable continue-on-error on a money path; charges, purchases, transfers, refunds, and other spend must fail fast.
142
-
143
- ## Subflows and convergence
144
-
145
- Define the called flow's inputs and outputs first. Then initialize a Run flow component and bind it:
146
-
147
- ```bash
148
- cai node set-flow <runFlowNodeId> --flow <calledFlowId> --json
149
- cai node props <runFlowNodeId> --json
150
- ```
151
-
152
- MCP: `cai node set-flow` maps to `node_subflow_set`.
153
-
154
- Setting the target flow creates mapping props from the target's current inputs. If those inputs later change, run `set-flow` again and reconfigure mappings.
155
-
156
- For one action after whichever exclusive branch runs, use `registry_flow-merge` (Join Paths) in `chooseActivePath` mode, then place the shared action after it. Never wire several branches directly to an ordinary node or duplicate the shared action. Follow the exact recipe in [references/patterns.md](references/patterns.md) and test each branch once.
157
-
158
- Return semantics: a flow that should return data needs `registry_flow-return-response`; without it callers see empty success. Declare outputs first, initialize that exact component after the producer, then bind its generated prop with `cai expr set`. Do not guess component keys. See [references/patterns.md](references/patterns.md). Multiple completed Return nodes merge, with later values overwriting earlier ones; design for one Return per path unless merging is intended.
159
-
160
- Deleting a flow removes its graph, contract, triggers, and caller links. Validate and restore if mistaken. Turning `continueOnError` off also deletes its Error output and edges; validate and restore likewise.
161
-
162
- Read [references/patterns.md](references/patterns.md) for concise canonical patterns.
163
-
164
- ## Before you finish
165
-
166
- ```bash
167
- cai branch list <controlNodeId> --json
168
- cai workflow validate --json
169
- cai run flow <flowId> --inputs <values.json> --json
170
- cai exec tree <workflowExecutionId> --json
171
- ```
172
-
173
- - Every branch has the intended condition and a wired target.
174
- - If/Else ordering matches priority and Else remains last.
175
- - Router fan-out is intentional when more than one condition can be true.
176
- - Loop item type and list expression compile against actual source data.
177
- - Expected true, false/Else, empty-list, one-item, several-item, over-limit, and error paths are tested.
178
- - The execution tree shows intended `only_when_false` or `upstream_skipped` reasons and no unexplained missing nodes.
39
+ Examples and commands: [command reference](references/command-reference.md). Control-flow details: [patterns](references/patterns.md).
@@ -0,0 +1,171 @@
1
+ # Controller AI flow control
2
+
3
+
4
+ Choose by intended runtime semantics.
5
+
6
+
7
+ - Guard the exact action with node `only-when`; see Conditions.
8
+ - Use If/Else for one path and Router for all matches; see Pick the correct primitive.
9
+ - Keep downstream wiring after node-only skip; see Conditions.
10
+ - Never invent a missing condition or Router order; see If/Else and Router.
11
+ - Build standard loops only with `flow for-each`; see Loops.
12
+ - Converge paths through Join Paths; see Subflows and convergence.
13
+ - Fail missing comparisons closed; keep money paths blocking. See Conditions, Loops, and the error pattern.
14
+ - Never acknowledge validation errors without explicit approval; see `controllerai-workflows` Lifecycle rules.
15
+
16
+ ## Pick the correct primitive
17
+
18
+ | Intent | Primitive | Semantics |
19
+ | --- | --- | --- |
20
+ | Skip one action when a condition is false | Node `only-when` | No alternate path |
21
+ | Choose exactly one path | If/Else | First matching branch runs; Else is last |
22
+ | Run every matching path | Router | Conditions are independent; multiple routes may run |
23
+ | Apply work to every list item | `flow for-each` | Atomic loop scaffold with typed item flow |
24
+ | Reuse a typed unit of work once | Run flow/subflow node | Explicit input and output contract |
25
+ | Continue after an action error | Error output or continue-on-error | Error behavior is explicit and testable |
26
+
27
+ Do not substitute one primitive merely because its component is easier to find.
28
+
29
+ ## Conditions
30
+
31
+ Before writing any condition:
32
+
33
+ ```bash
34
+ cai expr context <controlNodeId> --json
35
+ cai expr validate <controlNodeId> --mode onlyWhen --js '<boolean expression>' --json
36
+ ```
37
+
38
+ For one guarded action:
39
+
40
+ ```bash
41
+ cai node only-when <nodeId> --js '<boolean expression>' --json
42
+ ```
43
+
44
+ Use `--clear` only when the user's intent is to remove the guard.
45
+
46
+ Guard the action that must skip. Do not substitute If/Else for a node-only skip. Its `only_when_false` does not block ordinary output; downstream remains eligible. Keep wiring. Guard downstream separately or branch if it must also skip.
47
+
48
+ Node `only-when` takes one restricted boolean through `--js` or `--js-file`, or grouped conditions through `--groups-json '[["a","b"],["c"]]'` (inner arrays are AND, outer rows are OR). Root scalar `&&` or `||` inside one expression is unsupported; use `--groups-json` for compound guards. Grouped `string[][]` conditions exist only on branch add/update, with outer rows OR and inner entries AND. Use them only when branch semantics fit. If an exact compound node-only guard is required, report it as unsupported; do not weaken the rule or probe private APIs. For a missing number that must fail `< 5`, use a same-type boundary such as `(flow.unitsLeft ?? 5) < 5`.
49
+
50
+ Choose the primitive by the runtime signal that callers and tests need. A false node `only-when` records `skipReason: "only_when_false"`; a node on an untaken If/Else branch or unmatched Router route records `skipReason: "upstream_skipped"`.
51
+
52
+ ## If/Else and Router
53
+
54
+ Discover and initialize the appropriate control component using `controllerai-node-config`, then inspect its exact branch IDs:
55
+
56
+ ```bash
57
+ cai branch list <controlNodeId> --json
58
+ ```
59
+
60
+ A fresh If/Else already contains one blank `If` branch and an Else branch. The blank condition is a publish/runtime blocker: copy that default branch ID from `branch list` and update it before adding another condition branch. Do not leave the default blank and create a replacement beside it.
61
+
62
+ Never invent a missing condition from node names, current values, or one observed run. If no authoritative platform surface supplies the business rule, leave the branch blank, report the blocker, and ask the user.
63
+
64
+ ```bash
65
+ cai branch update <ifElseNodeId> \
66
+ --route <defaultIfBranchId> \
67
+ --name "<meaning>" \
68
+ --js '<boolean expression>' \
69
+ --json
70
+ ```
71
+
72
+ Add another condition only when the decision needs another non-Else case. Router routes are independent, so update an existing blank route or add each intended route explicitly:
73
+
74
+ ```bash
75
+ cai branch add <controlNodeId> --name "<meaning>" --js '<boolean expression>' --json
76
+ cai branch update <controlNodeId> --route <routeOrBranchId> --js '<boolean expression>' --json
77
+ cai branch connect <controlNodeId> --route <routeOrBranchId> --to <targetNodeId> --flow <flowId> --json
78
+ ```
79
+
80
+ For If/Else, order is behavior:
81
+
82
+ ```bash
83
+ cai branch reorder <ifElseNodeId> --order <conditionId1,conditionId2> --json
84
+ ```
85
+
86
+ List every condition branch exactly once and omit Else; the operation keeps Else last. Router routes cannot be reordered because every match runs. Do not simulate order by deleting a route or serializing its downstream nodes. Leave the fan-out unchanged unless the user explicitly authorizes a redesign.
87
+
88
+ After edits, run `cai branch list` again. Treat its warnings about missing conditions or unwired paths as blockers: an unwired selected path can end with an apparently green execution.
89
+
90
+ ## Loops
91
+
92
+ Never assemble the standard per-item subflow pattern by hand:
93
+
94
+ ```bash
95
+ cai node get <listProducingNodeId> --json
96
+ cai flow for-each \
97
+ --flow <parentFlowId> \
98
+ --from <listProducingNodeId> \
99
+ --source-output <exactOutputId> \
100
+ --name "<item flow name>" \
101
+ --item-type <text|number|date|boolean|custom.type> \
102
+ --json
103
+ ```
104
+
105
+ Use repeatable `--item-field <id:type[:display]>` only when creating a new `custom.*` item type.
106
+
107
+ Copy `--source-output` from the selected node's exact `outputs[].id`. It is an opaque ID, not `displayName`, and there is no first-output default. This matters for Router, If/Else, and error outputs as well as ordinary action nodes.
108
+
109
+ - If the source's whole output is the list, omit both `--list-field` and `--list-js`.
110
+ - If the list is nested in the source payload, use `--list-field` with the dot-delimited path below `.data`; the operation derives the not-yet-created loop input binding deterministically.
111
+ - Use `--list-js` only for an advanced exact expression. For multiline or shell-sensitive source, use `--js-file <path|->` instead. Both are mutually exclusive with `--list-field` and with each other; copy the wrapper/binding shape from expression context rather than guessing it.
112
+
113
+ MCP: pass expression source inline as `listJs` to `flow_for_each_scaffold`; the server cannot read a local `--js-file` path.
114
+
115
+ - The runtime cap defaults to 500 items and accepts 1 through 10000. Set `--max-items` explicitly when the product requirement differs. The scaffold operation creates the core graph first, then materializes and verifies this optional prop. The cap is all-or-nothing at runtime: a list larger than the cap throws and NOTHING iterates — there is no partial truncation.
116
+
117
+ Changing an existing loop: update its loop-node props directly:
118
+
119
+ ```bash
120
+ cai node set <loopNodeId> --prop maxItems --value <n> --json
121
+ cai node set <loopNodeId> --prop continueOnIterationError --value true --json
122
+ ```
123
+
124
+ MCP: use `node_prop_set` for both writes.
125
+
126
+ This is not `cai node continue-on-error`, which configures the node-level Error output — a different feature.
127
+
128
+ Read every returned warning. A list/item type mismatch means the scaffold is not verified even though it was created. Follow the returned `nextSteps` in order.
129
+
130
+ Inside loop mappings, `item` exists only where context exposes it. Inside the body flow, use its `flow.<itemInput>` binding. Downstream loop results are iteration records whose declared child outputs are under `.outputs`.
131
+
132
+ Loops run sequentially. `--continue-on-error` applies to a failed child iteration; a list expression or mapping/config expression that fails before the child starts still aborts the loop. Test empty, one-item, several-item, over-limit, and intended iteration-error behavior.
133
+
134
+ Never enable continue-on-error on a money path; charges, purchases, transfers, refunds, and other spend must fail fast.
135
+
136
+ ## Subflows and convergence
137
+
138
+ Define the called flow's inputs and outputs first. Then initialize a Run flow component and bind it:
139
+
140
+ ```bash
141
+ cai node set-flow <runFlowNodeId> --flow <calledFlowId> --json
142
+ cai node props <runFlowNodeId> --json
143
+ ```
144
+
145
+ MCP: `cai node set-flow` maps to `node_subflow_set`.
146
+
147
+ Setting the target flow creates mapping props from the target's current inputs. If those inputs later change, run `set-flow` again and reconfigure mappings.
148
+
149
+ For one action after whichever exclusive branch runs, use `registry_flow-merge` (Join Paths) in `chooseActivePath` mode, then place the shared action after it. Never wire several branches directly to an ordinary node or duplicate the shared action. Follow the exact recipe in [patterns.md](patterns.md) and test each branch once.
150
+
151
+ Return semantics: a flow that should return data needs `registry_flow-return-response`; without it callers see empty success. Declare outputs first, initialize that exact component after the producer, then bind its generated prop with `cai expr set`. Do not guess component keys. See [patterns.md](patterns.md). Multiple completed Return nodes merge, with later values overwriting earlier ones; design for one Return per path unless merging is intended.
152
+
153
+ Deleting a flow removes its graph, contract, triggers, and caller links. Validate and restore if mistaken. Turning `continueOnError` off also deletes its Error output and edges; validate and restore likewise.
154
+
155
+ Read [patterns.md](patterns.md) for concise canonical patterns.
156
+
157
+ ## Before you finish
158
+
159
+ ```bash
160
+ cai branch list <controlNodeId> --json
161
+ cai workflow validate --json
162
+ cai run flow <flowId> --inputs <values.json> --json
163
+ cai exec tree <workflowExecutionId> --json
164
+ ```
165
+
166
+ - Every branch has the intended condition and a wired target.
167
+ - If/Else ordering matches priority and Else remains last.
168
+ - Router fan-out is intentional when more than one condition can be true.
169
+ - Loop item type and list expression compile against actual source data.
170
+ - Expected true, false/Else, empty-list, one-item, several-item, over-limit, and error paths are tested.
171
+ - The execution tree shows intended `only_when_false` or `upstream_skipped` reasons and no unexplained missing nodes.