@controllerai/cli 0.2.18 → 0.2.21

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 (45) hide show
  1. package/package.json +6 -4
  2. package/skills/controllerai-agents/SKILL.md +66 -152
  3. package/skills/controllerai-agents/references/instructions-and-versioning.md +32 -34
  4. package/skills/controllerai-ai-nodes/SKILL.md +57 -65
  5. package/skills/controllerai-ai-nodes/references/selection-and-testing.md +44 -26
  6. package/skills/controllerai-data/SKILL.md +65 -118
  7. package/skills/controllerai-data/references/data-rails.md +24 -26
  8. package/skills/controllerai-expressions/SKILL.md +48 -117
  9. package/skills/controllerai-expressions/references/dsl-and-recipes.md +42 -40
  10. package/skills/controllerai-flow-control/SKILL.md +66 -122
  11. package/skills/controllerai-flow-control/references/patterns.md +23 -60
  12. package/skills/controllerai-integration-actions/SKILL.md +47 -75
  13. package/skills/controllerai-node-config/SKILL.md +60 -130
  14. package/skills/controllerai-node-config/references/prop-and-readiness.md +28 -78
  15. package/skills/controllerai-runs/SKILL.md +50 -85
  16. package/skills/controllerai-runs/references/debugging.md +24 -22
  17. package/skills/controllerai-start/SKILL.md +53 -58
  18. package/skills/controllerai-start/references/mcp-tool-map.md +21 -16
  19. package/skills/controllerai-triggers/SKILL.md +62 -159
  20. package/skills/controllerai-triggers/references/lifecycle.md +36 -48
  21. package/skills/controllerai-workflows/SKILL.md +70 -70
  22. package/skills/controllerai-workflows/references/command-map.md +19 -44
  23. package/skills/manifest.json +80 -77
  24. package/skills-mcp/controllerai-agents/SKILL.md +121 -0
  25. package/skills-mcp/controllerai-agents/references/instructions-and-versioning.md +76 -0
  26. package/skills-mcp/controllerai-ai-nodes/SKILL.md +92 -0
  27. package/skills-mcp/controllerai-ai-nodes/references/selection-and-testing.md +66 -0
  28. package/skills-mcp/controllerai-data/SKILL.md +119 -0
  29. package/skills-mcp/controllerai-data/references/data-rails.md +45 -0
  30. package/skills-mcp/controllerai-expressions/SKILL.md +98 -0
  31. package/skills-mcp/controllerai-expressions/references/dsl-and-recipes.md +136 -0
  32. package/skills-mcp/controllerai-flow-control/SKILL.md +114 -0
  33. package/skills-mcp/controllerai-flow-control/references/patterns.md +83 -0
  34. package/skills-mcp/controllerai-integration-actions/SKILL.md +89 -0
  35. package/skills-mcp/controllerai-node-config/SKILL.md +106 -0
  36. package/skills-mcp/controllerai-node-config/references/prop-and-readiness.md +49 -0
  37. package/skills-mcp/controllerai-runs/SKILL.md +88 -0
  38. package/skills-mcp/controllerai-runs/references/debugging.md +53 -0
  39. package/skills-mcp/controllerai-start/SKILL.md +80 -0
  40. package/skills-mcp/controllerai-start/references/mcp-tool-map.md +175 -0
  41. package/skills-mcp/controllerai-triggers/SKILL.md +112 -0
  42. package/skills-mcp/controllerai-triggers/references/lifecycle.md +59 -0
  43. package/skills-mcp/controllerai-workflows/SKILL.md +109 -0
  44. package/skills-mcp/controllerai-workflows/references/command-map.md +1 -0
  45. package/skills-mcp/manifest.json +189 -0
@@ -1,173 +1,104 @@
1
1
  ---
2
2
  name: controllerai-expressions
3
- description: Author, validate, persist, and debug Controller AI expressions for node props and conditions. Use when a value comes from a flow input, upstream node, loop item, workflow data, constant, calculation, template, or boolean rule, or when an expression compiles but resolves to null or the wrong input.
3
+ description: Author, validate, persist, and debug Controller AI expressions for node props and conditions. Use when a value comes from a flow input, an upstream node, a loop item, workflow data, a constant, a calculation, a template or a boolean rule, and when an expression compiles but resolves to null or the wrong input.
4
4
  ---
5
+ <!-- generated from .claude/skills by cli/scripts/render-skills.mjs; edit the source -->
5
6
 
6
- # Controller AI expressions
7
+ # Expressions
7
8
 
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.
9
+ ## Use this when
9
10
 
10
- 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.
11
+ - A node prop or condition needs a dynamic value: a flow input, a wired upstream output, the current loop item, workflow records, a date calculation, a text template, a boolean guard.
12
+ - An expression compiles but the run shows null, an empty value, or the wrong source.
11
13
 
12
- ## Rules that break tasks when skipped
14
+ Node discovery, connections and static props belong to `controllerai-node-config`; Router, If/Else and loop structure to `controllerai-flow-control`; execution evidence to `controllerai-runs`.
13
15
 
14
- - Wire first; refresh context after structural changes. See Required write loop.
15
- - `--value` is static; dynamic values use `--js`. See Typed values and templates.
16
- - Copy current bindings, not labels or stale names. See Read the destination's exact scope.
17
- - Never use raw ASTs, general JavaScript, or private APIs. See Use only advertised syntax.
18
- - Keep ternaries in the destination type. See Typed values and templates.
19
- - Prove resolution in a containing-flow run. See Verify runtime resolution.
16
+ ## How it works
20
17
 
21
- 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.
18
+ Expressions are written in a restricted JavaScript-like language and compiled into the engine's expression tree. It is not JavaScript: no loops, assignments, `new`, regular expressions, optional chaining, object literals (the `{ ignoreEmptyConstraints: true }` filter options object is the one accepted object) or library methods beyond what the compiler catalog advertises. A program is optional `const` declarations followed by one final expression.
22
19
 
23
- `.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.
20
+ **Everything is typed.** An expression can only be written against a known type. An integration node's output type is usually learned from a representative development run, or from a supported output selector or a declared provider sample, and `node status` says which route applies; `custom.*` types describe data-store records and AI structured outputs; `dataRecord.custom.x` is a stored record of that shape with its identity (`id`, `createdAt`, `updatedAt`), while bare `custom.x` is the shape alone; `list.` prefixes a list of any type. When an expression cannot be authored, the cause is usually a missing upstream type.
24
21
 
25
- ## How empty values evaluate
22
+ **Scope** comes from the destination's expression context: `flow.<name>` is a flow input, `inputs.<name>` a wired node input (usually already unwrapped), `item` the current loop item where the context exposes it, `workflowData("custom.type")` the records of an active collection on the current rail, and `now` and `isLiveVersion` constants. Bindings are display-derived and de-duplicated by the context; display labels and old spellings are not bindings.
26
23
 
27
- An empty input evaluates to its typed empty form: `""` for TEXT, null/undefined for objects. Both cases below fail silently:
24
+ **Empty values.** An omitted input arrives in its typed empty form: `""` for text, null for objects, and it is compared like any other value: `=== ""` matches only rows whose field is empty, `!==` passes every other row including rows with a missing value, `includes("")` matches every text row, and a negative membership check against a missing list passes everything. An omitted optional input therefore does not close a filter by itself: equality usually returns nothing, a negation or containment returns everything, and an omitted lookup key can turn a create-if-missing step into a duplicate. With `ignoreEmptyConstraints` on, a constraint whose value is null, empty text or an empty list is skipped; `0`, `false`, whitespace and dates stay real constraints. `.every()` takes the same predicates as `.filter()` and compiles to a single-pass "all match"; a constraint on a field the item does not have compiles, makes `.every()` false and is dropped by `.filter()`, which then matches every item. Predicates compare a field of the item, so a list of numbers or text cannot be filtered by its values: count or classify primitives with a per-item loop that returns a flag, then sum the flags. Comparators decide what missing, null or unparseable values do, and negative comparisons can pass rows with missing values, so a guard states presence explicitly.
28
25
 
29
- - **A filter compared with empty matches no records.** To skip an omitted constraint, opt into `ignoreEmptyConstraints`; see [references/dsl-and-recipes.md](references/dsl-and-recipes.md).
30
- - **An omitted optional TEXT input arrives as `""`.** Equality then matches nothing and can make a create-if-missing step duplicate data.
26
+ **Dates** travel as ISO-8601 text. A `date`-typed input or field takes date operations directly; text needs `.toDate()` first, and epoch text needs an explicit unit, `.toDate("epoch_seconds")` or `.toDate("epoch_millis")`. Format with Moment tokens such as `format("YYYY-MM-DD")`.
31
27
 
32
- When a run unexpectedly returns zero results, check the resolved configs for empty comparison values first (`cai exec node <nodeExecutionId> --full`).
28
+ **Templates and ternaries.** A backtick template with `${...}` returns text; keep non-text values as direct typed expressions. A ternary keeps its type when both branches are numbers or both are booleans; any other pair renders as text, so give both branches the same type when a number or boolean is needed, and never quote or coerce a branch to hide a type mismatch.
33
29
 
34
- ## Required write loop
30
+ **Channels.** A literal value and an expression are stored on different channels; the setters pick the channel from the prop's metadata. On a value-channel text array, a scalar expression replaces the whole array with one dynamic element and a list expression is rejected; a static list is written as a literal array, and a list expression only where the prop exposes whole-array expression mode.
35
31
 
36
- ### 1. Wire first
32
+ ## Rules
37
33
 
38
- 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.
34
+ 1. **Wire first, then read the context, then write.** An upstream output listed under `unwiredUpstreamOutputs` is discovery, not scope. Re-read the context after rewiring, renaming an input, changing a schema, changing a dynamic selector, or resolving an output type.
35
+ 2. **Copy bindings exactly.** Never derive one from a label, never add `.data` on your own, and when a diagnostic advertises a typeable accessor, use it. For duplicate names, use the de-duplicated binding, input ID and source node the context gives.
36
+ 3. **Use only advertised operations.** Read the catalog for the type; never author a raw expression tree or probe a private API. When an idiom is rejected, use the advertised alternative, or express the logic through routing, typed inputs or a native action.
37
+ 4. **A literal is a literal.** `flow.name`, `{{name}}` and `${name}` inside a literal stay text. Dynamic values go through the expression setter.
38
+ 5. **Preserve `0` and `false`.** Fallbacks are same-type literals (`flow.count ?? 0`, `flow.enabled ?? false`) and never swallow a real zero or false.
39
+ 6. **Optional filters are a decision, not a default.** An omitted value is a real comparison, not a skipped constraint; turn on `ignoreEmptyConstraints` only when omitting that constraint is what the user wants, and test both the supplied and the omitted case.
40
+ 7. **Compiler success proves syntax and types, not the source.** When names are ambiguous, check the source node and input IDs, then look at runtime values.
41
+ 8. **An expression that reads `inputs.<name>` is verified only by running the containing flow.** A node run never executes upstream nodes.
42
+ 9. **Empty results need a reason.** An intentional empty (a closed filter, an absent optional) is valid; an unexpected null or partial value in `resolvedConfigs` is a failure to trace, not something to fix by rewriting valid syntax.
39
43
 
40
- ```bash
41
- cai node connect --flow <flowId> --from <sourceNodeId> --to <targetNodeId> --json
42
- ```
43
-
44
- Use `cai branch connect` for Router or If/Else outputs.
44
+ ## Recipes
45
45
 
46
- ### 2. Read the destination's exact scope
46
+ ### Discover the scope and the operations
47
47
 
48
48
  ```bash
49
49
  cai expr context <nodeId> --prop <propName> --json
50
- ```
51
-
52
- Copy binding and field names exactly. Important sources include:
53
-
54
- - `flow.<name>`: flow input
55
- - `inputs.<name>`: wired node input; the payload is usually already unwrapped
56
- - `item`: injected current item, only where context exposes it
57
- - `workflowData("custom.type")`: records from an active workflow collection
58
- - `now`, `isLiveVersion`: constants shown by context
59
-
60
- 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.
61
-
62
- When compilation reports an unknown field, copy a typeable display-derived accessor it advertises; field IDs appear second for disambiguation.
63
-
64
- ### 3. Use only advertised syntax
65
-
66
- Get the current operation catalog when needed:
67
-
68
- ```bash
69
50
  cai expr ops --workflow <workflowId> --type <text|number|date|list|boolean|file|custom> --json
70
51
  ```
71
52
 
72
- 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`.
53
+ The catalog is read from the workflow's expression context, so the workflow needs at least one node, and an existing pin (`cai use` or `CAI_WORKFLOW`) can replace `--workflow`.
73
54
 
74
- 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.
75
-
76
- 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.
77
-
78
- #### Typed values and templates
79
-
80
- `--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.
81
-
82
- 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.
83
-
84
- Read [references/dsl-and-recipes.md](references/dsl-and-recipes.md) for canonical patterns.
85
-
86
- ### 4. Compile, write, and read back
87
-
88
- For a node prop, prefer:
55
+ If the source is not wired yet:
89
56
 
90
57
  ```bash
91
- cai expr set <nodeId> --prop <propName> --js '<expression>' --json
58
+ cai node connect --flow <flowId> --from <sourceNodeId> --to <targetNodeId> --json
92
59
  ```
93
60
 
94
- MCP: use `node_prop_set` with `js`; it merges `cai expr set` with `cai node set`.
61
+ Router and If/Else outputs connect through `cai branch connect`.
95
62
 
96
- 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`.
63
+ ### Validate, write, read back
97
64
 
98
65
  ```bash
99
- cai expr set <nodeId> --prop <propName> --js-file expression.js --json
66
+ cai expr validate <nodeId> --prop <propName> --js '<expression>' --json
67
+ cai expr set <nodeId> --prop <propName> --js '<expression>' --json
68
+ cai expr decompile <nodeId> --prop <propName> --json
100
69
  ```
101
70
 
102
- MCP: pass the file contents inline as `js`; the hosted process does not read the local path or stdin.
71
+ The setter fetches the prop's metadata, enables a catalog-only optional prop when needed, compiles, writes the right channel and verifies the readback. If the write fails after enabling, the prop stays enabled: disable it with `cai node disable-prop <nodeId> --prop <propName>` and re-read. Decompile returns the stored source and channel, not a type: validate the decompiled source against the current destination to confirm the type still holds.
103
72
 
104
- 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>`.
73
+ For a multiline or shell-sensitive expression, keep the exact UTF-8 source in a file and pass `--js-file <path>` (or `--js-file -` for stdin) instead of `--js`; `expr validate`, `expr set`, `node set`, `node only-when`, `branch add` and `branch update` all accept it.
105
74
 
106
- MCP: repair with `node_optional_prop_set` and `enabled: false`.
107
-
108
- 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.
109
-
110
- For a side-effect-free check:
111
-
112
- ```bash
113
- cai expr validate <nodeId> --prop <propName> --js '<expression>' --json
114
- ```
115
-
116
- For conditions:
75
+ ### Conditions
117
76
 
118
77
  ```bash
119
78
  cai expr validate <nodeId> --mode onlyWhen --js '<boolean expression>' --json
120
79
  cai node only-when <nodeId> --js '<boolean expression>' --json
121
- ```
122
-
123
- 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.
124
-
125
- ```bash
80
+ cai node only-when <nodeId> --groups-json '[["flow.email.isNotEmpty()","flow.units < 5"],["flow.override === true"]]' --json
126
81
  cai branch update <nodeId> --route <routeId> --condition-json '[["flow.email.isNotEmpty()","flow.units < 5"],["flow.override === true"]]' --json
127
82
  ```
128
83
 
129
- 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`.
130
-
131
- Router and If/Else conditions are written with `cai branch add` or `cai branch update` after the same context/validation discipline.
84
+ A node guard takes one boolean expression, or groups where each inner list is AND and the outer list is OR; a root `&&` or `||` inside one expression is not supported, so compound guards use groups. Branch conditions use the same grouped shape, each string compiled separately. Make a missing number fail a comparison with a same-type boundary: `(flow.unitsLeft ?? 5) < 5`.
132
85
 
133
- ### 5. Verify runtime resolution
86
+ ### Prove the value at runtime
134
87
 
135
88
  ```bash
136
- cai expr decompile <nodeId> --prop <propName> --json
137
89
  cai run node <nodeId> --inputs <values.json> --json
138
- ```
139
-
140
- MCP: `run_node` takes the parsed `inputs` object and `waitSeconds` (default 60, max 120).
141
-
142
- 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:
143
-
144
- ```bash
145
90
  cai run flow <flowId> --inputs <values.json> --json
91
+ cai exec node <nodeExecutionId> --full --json
146
92
  ```
147
93
 
148
- MCP: `run_flow` takes the parsed `inputs` object and `waitSeconds` (default 60, max 120).
149
-
150
- 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.
94
+ The node run isolates the destination's own configuration. The flow run proves an upstream-derived value. Inspect `runScope` and `resolvedConfigs`; when a search unexpectedly returns nothing, look at the resolved comparison values first.
151
95
 
152
- ## Hard rules
96
+ ## Verify
153
97
 
154
- - Never guess a binding, field, method, or target type.
155
- - Never author against an unwired output.
156
- - Never add `.data` automatically. Use the exact path shown by context.
157
- - Re-read context after rewiring, renaming inputs, changing schemas, changing dynamic selectors, or resolving an output type.
158
- - Preserve `0` and `false`; they are values, not missing data.
159
- - Optional filters need an explicit behavior decision. Do not silently change fail-closed filtering to match-all.
160
- - 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.
98
+ The current context shows the intended source IDs and bindings; the decompiled source validates against the current destination; the run's `resolvedConfigs` hold the intended value, with intentional empties explained; the edge cases the task implies were exercised: omitted input, `0`, `false`, empty text or list, missing record.
161
99
 
162
- ## Before you finish
100
+ ## Report
163
101
 
164
- ```bash
165
- cai expr context <nodeId> --prop <propName> --json
166
- cai expr validate <nodeId> --prop <propName> --js '<expression>' --json
167
- cai expr decompile <nodeId> --prop <propName> --json
168
- cai run flow <flowId> --inputs <values.json> --json
169
- ```
102
+ Which values are dynamic and what they read from, any optional-filter or fallback decision you made, and what the run showed.
170
103
 
171
- - Context must show current source IDs and bindings.
172
- - Validation and decompile must agree on source and type.
173
- - The flow run must show the intended value in `runScope` and `resolvedConfigs`.
104
+ Read [references/dsl-and-recipes.md](references/dsl-and-recipes.md) for the canonical patterns: templates, prefix tests, HTTP JSON bodies, fallbacks, dates, workflow records, optional filters, loop items and outputs, and the troubleshooting order.
@@ -1,6 +1,7 @@
1
+ <!-- generated from .claude/skills by cli/scripts/render-skills.mjs; edit the source -->
1
2
  # Expression DSL and recipes
2
3
 
3
- The live compiler and `cai expr context`/`cai expr ops` are authoritative. These examples show structure, not names to copy blindly.
4
+ The live compiler, `cai expr context` and `cai expr ops` are authoritative. These examples show structure; take every binding, field and operation name from the context.
4
5
 
5
6
  ## Text template
6
7
 
@@ -8,36 +9,25 @@ The live compiler and `cai expr context`/`cai expr ops` are authoritative. These
8
9
  `Hello ${flow.firstName.trim()}, order ${inputs.order.id} is ready`
9
10
  ```
10
11
 
11
- Use the exact bindings and fields returned for the destination node.
12
-
13
12
  ## Text prefix
14
13
 
15
- When `.startsWith()` is unavailable, test a prefix with:
14
+ When `.startsWith()` is not advertised, test a prefix with a guarded split:
16
15
 
17
16
  ```js
18
17
  flow.subject.isNotEmpty() ? flow.subject.split("urgent")[0] === "" : false
19
18
  ```
20
19
 
21
- The typed conditional keeps empty text from matching. Replace the binding and prefix literal with exact values from context. Do not split on whitespace unless the requirement is first-word equality. Test punctuation immediately after the prefix, such as `urgent-alpha`, plus a near miss.
20
+ The guard keeps empty text from matching. Prefix matching is not first-word matching: do not split on whitespace unless first-word equality is the requirement, and test a value with punctuation right after the prefix (`urgent-alpha`) plus a near miss.
22
21
 
23
22
  ## HTTP JSON body
24
23
 
25
- An HTTP node's JSON/body prop is stored as JSON text. The expression language does not support object literals or `JSON.stringify`. Build the document as a template and insert complete JSON tokens:
24
+ A JSON body prop is JSON text. The language has no constructed object literals and no `JSON.stringify`, so build the document as a template of complete JSON tokens:
26
25
 
27
26
  ```js
28
27
  `{"message":${flow.name.jsonEncode()},"payload":${flow.user.stringify()}}`
29
28
  ```
30
29
 
31
- - Use `.jsonEncode()` for text or boolean values. It returns a JSON-safe token, including the quotes around text, so do not add another pair of quotes around `${...}`.
32
- - Use `.stringify()` for a custom object or list.
33
- - Keep JSON punctuation outside the interpolation and validate against the destination's exact prop before writing.
34
-
35
- ```bash
36
- cai expr validate <httpNodeId> --prop <jsonBodyProp> --js-file body-expression.js --json
37
- cai expr set <httpNodeId> --prop <jsonBodyProp> --js-file body-expression.js --json
38
- ```
39
-
40
- MCP: pass the file contents inline as `js`; the set call uses `node_prop_set`.
30
+ `.jsonEncode()` returns a JSON-safe token: quoted for text, bare `true`/`false` for a boolean, so never add a second pair of quotes. `.stringify()` serializes a custom object or list. Keep the JSON punctuation outside the interpolations and validate against the exact body prop before writing.
41
31
 
42
32
  ## Same-type fallback
43
33
 
@@ -47,7 +37,7 @@ flow.label ?? "unknown"
47
37
  flow.enabled ?? false
48
38
  ```
49
39
 
50
- Fallbacks must be same-type literals. `0` and `false` remain real values.
40
+ `0` and `false` are values; a fallback never replaces them.
51
41
 
52
42
  ## Boolean condition
53
43
 
@@ -56,7 +46,7 @@ flow.email.isNotEmpty()
56
46
  inputs.score >= 80
57
47
  ```
58
48
 
59
- Validate conditions with `--mode onlyWhen` and without a prop name.
49
+ Validate a condition with `--mode onlyWhen` and no prop name.
60
50
 
61
51
  ## Dates
62
52
 
@@ -65,23 +55,23 @@ inputs.createdAt.toDate() > now.plusDays(-7)
65
55
  inputs.createdAt.toDate().format("YYYY-MM-DD")
66
56
  ```
67
57
 
68
- Convert text to a date before date math. Use negative amounts to move backward.
58
+ Convert text to a date before date math; a negative amount moves backward. Use Moment format tokens, never JavaScript `Date` constructors or getters.
69
59
 
70
60
  ## Workflow records
71
61
 
72
- Filter immediately after `workflowData` so the engine can push supported predicates into storage:
62
+ Filter immediately after `workflowData` so supported predicates are pushed into storage, then sort by an advertised field:
73
63
 
74
64
  ```js
75
65
  workflowData("custom.lead")
76
66
  .filter(row => row.status === "active")
77
- .sortBy("created_at", "desc")
67
+ .sortBy("createdAt", "desc")
78
68
  ```
79
69
 
80
- The collection must exist on the current dev/live rail. A schema declaration alone does not guarantee active records.
70
+ The collection must exist on the current rail; a schema declaration alone does not put rows in it. Record identity fields are `id`, `createdAt`, `updatedAt`.
81
71
 
82
72
  ## Optional filter inputs
83
73
 
84
- Filters are fail-closed by default: a missing right-hand value matches no records. Use optional matching only when the user's requirement explicitly allows an omitted constraint:
74
+ An empty right-hand value is a real comparison: equality matches only empty fields, a negation or containment matches every row. Skip an omitted constraint only when the requirement allows it:
85
75
 
86
76
  ```js
87
77
  workflowData("custom.lead").filter(
@@ -90,45 +80,57 @@ workflowData("custom.lead").filter(
90
80
  )
91
81
  ```
92
82
 
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.
83
+ With the option on, null, empty text and an empty list skip the constraint; `0`, `false`, whitespace and dates remain real constraints. Test the supplied and the omitted case.
84
+
85
+ ## List membership
86
+
87
+ For a list field such as tags, membership is a single element: `row.tags.includes(flow.tag)`. `filter`, `some`, `find` and `every` share the same predicate grammar; `map` projects fields rather than building objects.
88
+
89
+ ## Primitive lists
90
+
91
+ A predicate compares a field of the item, so `flow.weights.filter(w => w >= flow.threshold)` is rejected, and `w.value` on a number compiles as a missing field whose constraint is dropped, matching every item. Aggregate a primitive list with the advertised list operations (`.sum()`, `.count()`, `.max()`), and classify or count its items with a per-item loop whose body returns a flag:
92
+
93
+ ```js
94
+ inputs.runFlowOnList.filter(row => row.status === "completed").map(row => row.outputs).map(row => row.heavyFlag).sum()
95
+ ```
96
+
97
+ `.map()` picks one field per call, so nested picks are chained; the `status` filter keeps failed iterations out of the total.
94
98
 
95
99
  ## Loop item
96
100
 
97
- Where context exposes `item`, primitive lists use the item directly:
101
+ Where the context exposes `item`, a primitive list uses it directly:
98
102
 
99
103
  ```js
100
104
  item
101
105
  ```
102
106
 
103
- Object lists use an advertised field:
107
+ An object list uses an advertised field:
104
108
 
105
109
  ```js
106
110
  item.email
107
111
  ```
108
112
 
109
- Inside the child flow, the item arrives through a flow input and is referenced as `flow.<name>`, not `item`.
113
+ Inside the child flow the item arrives as a flow input and is `flow.<name>`, never `item`. A root routing node can read flow inputs directly; no producer node is needed to expose them.
110
114
 
111
115
  ## Loop output
112
116
 
113
- Run-on-list output is a list of iteration records. Child flow outputs live under `outputs`:
117
+ Run-on-list output is a list of iteration records; child outputs sit under `outputs`:
114
118
 
115
119
  ```js
116
120
  inputs.enrichEach[0].outputs.score
117
121
  ```
118
122
 
119
- Confirm the exact binding and field path in context.
123
+ Confirm the binding and the path in the context.
120
124
 
121
- ## Troubleshooting sequence
125
+ ## Troubleshooting order
122
126
 
123
- 1. `cai expr context <node> --prop <prop> --json`
124
- 2. Confirm the intended source is wired and its `sourceNodeId` is correct.
125
- 3. `cai expr validate ...`
126
- 4. `cai expr set ...`
127
- 5. `cai expr decompile ...`
128
- 6. `cai run node ... --json` for isolated destination configuration
129
- 7. `cai run flow <flowId> ... --json` when the expression consumes a wired upstream input
127
+ 1. `cai expr context <nodeId> --prop <propName> --json`
128
+ 2. Confirm the intended source is wired and its `sourceNodeId` is right.
129
+ 3. `cai expr validate <nodeId> --prop <propName> --js '<expression>' --json`
130
+ 4. `cai expr set <nodeId> --prop <propName> --js '<expression>' --json`
131
+ 5. `cai expr decompile <nodeId> --prop <propName> --json`
132
+ 6. `cai run node <nodeId> --inputs <values.json> --json` for the destination alone
133
+ 7. `cai run flow <flowId> --inputs <values.json> --json` when the expression consumes a wired input
130
134
  8. `cai exec node <nodeExecutionId> --full --json`
131
135
 
132
- MCP: step 4 uses `node_prop_set`; pass expression source and run inputs inline, and set `waitSeconds` on `run_node` or `run_flow` from 0 through 120.
133
-
134
- If compilation succeeds but runtime is null or partially empty, inspect `runScope`, the actual input value, and `resolvedConfigs`; do not keep rewriting syntax blindly. A node-only run cannot prove an `inputs.<name>` binding because it does not execute upstream nodes.
136
+ If compilation succeeds but the runtime value is empty or partial, inspect `runScope`, the actual input value and `resolvedConfigs` before touching the syntax again. A node-only run cannot prove an `inputs.<name>` binding because it does not execute upstream nodes.