@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
@@ -1,184 +1,59 @@
1
1
  ---
2
2
  name: controllerai-data
3
- description: Define Controller AI custom data types and safely read, write, import, copy, and debug workflow records across development and live stores. Use when a workflow needs durable state, custom structured types, collections, record queries, CSV import, workflowData expressions, or data migration between dev and live.
3
+ description: Build durable application state in Controller AI workflow data stores: schemas, record identity, CRUD flows, search, partial updates, relationships, imports and development/live data. Use for CRM backends, trackers, data APIs and persistence.
4
4
  ---
5
5
 
6
- # Controller AI data
6
+ # Workflow data as an application backend
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
+ Each workflow can own custom schemas and durable collections. Its flows share those collections, so an agent can use a set of flows as a CRM or application API. Creating a schema makes the collection available; creating a flow output only returns a value and does not store it.
9
9
 
10
- Controller AI has two separate concerns:
10
+ `custom.contact` describes a shape. `dataRecord.custom.contact` is a stored record of that shape, including `id`, `createdAt` and `updatedAt`. Lists of fetched records preserve identity. Use real fetched records as inputs to native Get/Update/Delete actions; an object that merely looks like a contact does not prove record identity.
11
11
 
12
- - A `custom.*` schema defines fields and makes a collection available.
13
- - Development and live record stores hold separate rows. Publishing graph/configuration does not copy records.
12
+ ## What is available
14
13
 
15
- ## Rules that break tasks when skipped
14
+ - Native Create, Get, Update, Update Multiple and Delete data-record actions.
15
+ - `workflowData("custom.contact")` expressions for query/filter/sort/count and selecting records.
16
+ - Typed record relationships, separate dev/live records, schema changes, CSV import validation/commit, and explicit copy-to-live.
17
+ - MCP data tools for inspecting, creating, querying, patching and deleting records during setup or debugging. Native workflow nodes provide the reusable capability an agent calls later.
16
18
 
17
- - Persisting data requires both a schema and a create-record node; see A type vs a stored record.
18
- - `schema update --field` replaces the full field list; see Define a schema deliberately.
19
- - Relabel a stable key instead of recreating it; see How schema changes affect stored records.
20
- - Query results and writes use different field keys; see Respect field-key asymmetry.
21
- - Read and write the requested dev or live store; see Work on the correct rail.
22
- - Remove only scratch schemas created for abandoned attempts; see Before you finish.
19
+ Discover the current `registry_data` action keys and schemas. Do not infer unsupported database guarantees: unique business keys, permissions, relationship deletion policy and concurrent business rules need deliberate application logic or verified platform support.
23
20
 
24
- ## A type vs a stored record
21
+ ## Recipe: CRM flows for an agent
25
22
 
26
- When a request says store, save, log, or record with no external destination, create the schema and configure a create-record node. A flow output alone is not persistence.
23
+ 1. Create schemas with stable field IDs: companies, contacts, deals and activities. Use fields for business data, record references for relationships where appropriate, and a clear status/archive policy.
24
+ 2. Define create/get/update/delete/search flow contracts per entity. Include useful descriptions and explicit results for invalid inputs, missing records and duplicate business keys. Do not make an empty record look like success.
25
+ 3. Create validates input and referenced records before writing. Get selects by record ID. Update fetches the record and patches only requested fields. Delete applies the chosen relationship policy. Search returns matching records and count/page information.
26
+ 4. Add useful composite reads such as “contact plus company, deals and activity history.” Prefer explicit projections over large unbounded record graphs.
27
+ 5. Test create → get → search → update → get → delete → missing get. Also test duplicate keys, invalid references, omitted filters, empty matches, zero/false values, list membership, clearing a field and page boundaries.
28
+ 6. Attach each tested flow as an agent tool when a conversational CRM agent is requested.
27
29
 
28
- `custom.customer` and `dataRecord.custom.customer` are different things, and the difference carries through everything:
30
+ ## Search expressions
29
31
 
30
- - `custom.customer` is the TYPE — the shape, like a TypeScript interface. A value of this type is just data in that shape.
31
- - `dataRecord.custom.customer` is an ACTUAL STORED RECORD of that shape — the same fields plus record identity (`id`, `createdAt`, `updatedAt`) proving it lives in the collection.
32
+ Use current `expr_context` bindings. Typical sources and operations:
32
33
 
33
- In the product UI this appears as the "data record" checkbox when selecting a type for a flow input or run-subflow-on-list: unchecked means "data shaped like a customer", checked means "a real stored customer record". Users find this checkbox confusing — be ready to explain it in these terms.
34
-
35
- Consequences:
36
-
37
- - Only real records satisfy a `dataRecord.*` input. An expression that CONSTRUCTS record-shaped objects (e.g. a `.map()` that builds new objects) produces `custom.*`-shaped values with no identity — pass fetched records through instead.
38
- - Record metadata (`id`, `createdAt`) exists only on `dataRecord.*` values; on plain `custom.*` values those paths resolve empty.
39
-
40
- ## Data nodes: update semantics
41
-
42
- - "Update multiple data records" takes an expression typed `list.dataRecord.custom.<type>` — real stored records passed through from a query or `workflowData(...)`. The batch is ONE transaction: if any resolved record is stale (deleted since fetched) or foreign to the collection, the node errors and NOTHING updates. A failed run therefore means zero changes — fix and rerun safely.
43
- - On the single-record "Update data" node, every ENABLED field is written. Enable only the fields you intend to change; disabled fields keep their stored values.
44
-
45
- ## Define a schema deliberately
46
-
47
- Inspect existing types first:
48
-
49
- ```bash
50
- cai schema list --json
51
- cai schema get custom.<name> --json
52
- ```
53
-
54
- Create a type with stable field IDs:
55
-
56
- ```bash
57
- cai schema create custom.customer \
58
- --display "Customer" \
59
- --field email:text:"Email" \
60
- --field score:number:"Score" \
61
- --json
62
- ```
63
-
64
- Creating the schema creates/synchronizes its collection. `cai data collection create` is only an alias for schema creation.
65
-
66
- MCP: `cai data collection create` is merged into `schema_type_create`.
67
-
68
- `schema update --field` replaces the complete field list; it does not append — this is deliberate, so a subset CAN intentionally replace the whole schema. Read the current schema, preserve intended fields, and disclose any removals. The transport response warns about every field being dropped and echoes back the COMPLETE stored shape after the write — always read that echo and confirm it matches intent. Deleting a schema hides its collection and records and breaks dependent expressions — tell the user, and know it is recoverable (see below).
69
-
70
- ## How schema changes affect stored records
71
-
72
- Schema changes never hard-delete data. There is no delete path for records in schema sync — everything is deactivation and hiding:
73
-
74
- - Delete a type → its collection goes INACTIVE on the next saved state; every record row survives. Re-add the same `custom.*` key and the same collection reactivates with all records intact.
75
- - Delete a field → its values stay inside each record, unaddressable. Re-add the SAME field id and the old values reappear.
76
- - Add a field → purely additive; existing records read the new field as null.
77
- - Change a field's type → values are never converted or erased; incompatible values read as null and return if the type is changed back. The schema hash changes, which blocks copy-to-live until dev and live match again.
78
- - Publish applies the same deactivate/reactivate logic to live. Live records are hidden, never deleted.
79
- - Restoring an older workflow state rolls back the DEFINITION (graph and schemas), never the data. The record store is not versioned and only moves forward: a deleted type reactivates with whatever the store holds NOW — records created, edited, or soft-deleted after the restored state stay exactly as they are. Restore is not a data snapshot.
80
-
81
- The truly destructive operations are record-level only: `data delete`, `data delete-many`, and overwriting edits. Those have no undo — treat them with explicit user intent; everything schema-shaped is safely reversible.
82
-
83
- Relabel a type without changing its stable key:
84
-
85
- ```bash
86
- cai schema update custom.customer --display "Customers" --json
34
+ ```javascript
35
+ workflowData("custom.contact").filter(r => r.id === flow.recordId).first()
36
+ workflowData("custom.contact").filter(r => r.owner === flow.owner, { ignoreEmptyConstraints: true })
37
+ workflowData("custom.contact").filter(r => r.tags.includes(flow.tag))
38
+ workflowData("custom.contact").skip(flow.offset).take(flow.limit)
87
39
  ```
88
40
 
89
- Changing `custom.customer` itself is a key migration, not a relabel. It creates a new type and deactivates the old collection, stranding its records under the old key. Migrate records explicitly by export/import or restore the old key.
41
+ `ignoreEmptyConstraints` skips absent values, empty text and empty lists. `false` and `0` remain actual filter values. Without the option, an empty value remains a real comparison. List `.includes(value)` takes an **element**, not a list of elements. A text `.includes` is case-sensitive; for case-insensitive search, store normalized search text and normalize the query before comparing. Test the exact filter against representative rows.
90
42
 
91
- ## Respect field-key asymmetry
43
+ Use `.take(n)` for a count and `.skip(n)` for an offset. Legacy `.itemsFrom(n)`/`.itemsUntil(n)` use one-based boundaries: `.itemsUntil(51)` returns the first 50. Keep page counts and limits explicit; do not label a truncated list as a complete export.
92
44
 
93
- ```bash
94
- cai data fields --type custom.customer --json
95
- ```
96
-
97
- - `data create` and `data update` take field IDs.
98
- - Query/get results are keyed by field display names.
99
- - CSV columns are also matched by display names.
100
-
101
- Never feed a queried record directly into a write. Build a new fields object keyed by the `writeKey` mapping and omit reserved `id`, `createdAt`, and `updatedAt`.
45
+ ## Partial updates
102
46
 
103
- Fields may be typed `date`. Their values are strict ISO-8601 strings (`"2026-06-05"` or `"2026-06-05T12:30:00Z"`) on both write and read; anything else is rejected. Reserved `createdAt`/`updatedAt` are currently `text` — use `.toDate()` before date operations on them.
47
+ Single Update writes every enabled field by default. Enable only intended fields for a fixed patch. For a dynamic API patch, enable the optional `$fieldMask` prop, bind it to a `list.text` of schema field IDs, and enable/configure the candidate fields. Only selected fields are written in one update. An empty mask selects none; an unknown or disabled selected field is an error. Omit the mask to keep the existing enabled-field behavior.
104
48
 
105
- ## Work on the correct rail
106
-
107
- Development is the default:
108
-
109
- ```bash
110
- cai data collections --env dev --json
111
- cai data query --type custom.customer --env dev --json
112
- cai data create --type custom.customer --fields '{"email":"a@example.com","score":5}' --env dev --json
113
- ```
49
+ This supports list/date/scalar updates without one node per field. Selected `false`, `0`, empty text and empty lists are real updates; omitted mask entries preserve the stored values. Distinguish clearing from omission in the flow contract. Update Multiple applies a patch transactionally to the selected real records; verify its failure behavior for stale or foreign IDs.
114
50
 
115
- MCP: every data write requires explicit `env`; reads default to `dev` when `env` is omitted.
51
+ ## Keys, schemas and environments
116
52
 
117
- Use `--env live` only when the user intends to inspect or mutate production state. Before a live write, say that published/triggered runs read this store.
53
+ Inspect `data_field_list` for write keys. Data-tool writes use stable field IDs; query results can use display labels. Construct writes from the mapping instead of feeding a returned display-keyed object back into a write. Renaming a label is different from changing the stable schema/field key.
118
54
 
119
- `data query --query` is plain-text search over serialized records and metadata, not a field predicate. Perform exact field filtering inside workflow expressions or inspect results explicitly.
55
+ Schema field updates replace the full declared field list. Read the existing schema and include every field intended to remain. Removing a schema or field hides its stored data; it does not snapshot or erase the underlying records. Restoring workflow history restores the definition only.
120
56
 
121
- ## Delete records explicitly
122
-
123
- Record deletion is available only through the control surface; no workflow data node exposes it. Delete one known record or an explicit set of IDs:
124
-
125
- ```bash
126
- cai data delete <recordId> --type custom.customer --env dev --json
127
- cai data delete-many --type custom.customer --records <id1,id2> --env dev --json
128
- ```
129
-
130
- Delete every record in a collection only with explicit user intent; `--all` also requires `--yes`:
131
-
132
- ```bash
133
- cai data delete-many --type custom.customer --all --yes --env dev --json
134
- ```
135
-
136
- MCP: `data_record_delete_many` requires explicit `env: dev|live` and exactly one of `recordIds` or `all: true`; it has no `yes` parameter.
137
-
138
- The CLI default rail is development. Targeted and `--all --yes` bulk deletions also support `--env live`; a live deletion immediately changes the data read by the published workflow, so use it only with explicit production intent and query that rail afterward.
139
-
140
- ## Import and promote
141
-
142
- Validate CSV before writing:
143
-
144
- ```bash
145
- cai data import --type custom.customer --file customers.csv --mode dryRun --env dev --json
146
- cai data import --type custom.customer --file customers.csv --mode commit --env dev --json
147
- ```
148
-
149
- MCP: pass CSV text inline as `content` with explicit `env: dev|live` to `data_record_import_validate`, then use `data_record_import_commit` to write the validated content.
150
-
151
- Both CLI modes support `--env live`: dry-run remains non-writing, while a live commit appends rows immediately visible to the published workflow. Verify result counts and query the imported rows on the same rail.
152
-
153
- To seed live from development:
154
-
155
- ```bash
156
- cai workflow publish --json
157
- cai data copy-to-live --type custom.customer --json
158
- cai data collections --env live --json
159
- ```
160
-
161
- MCP: call `data_copy_to_live` with the explicit acknowledgement `env: live`.
162
-
163
- The live schema must exist through a publish before copying. Treat copy-to-live as a production mutation and report counts/results.
164
-
165
- ## Workflow expressions
166
-
167
- `workflowData("custom.customer")` requires an active collection on the executing rail. Read `controllerai-expressions` for filtering. Test stateful behavior against development, then independently verify required live seed data.
168
-
169
- Read [references/data-rails.md](references/data-rails.md) before schema rewrites, deletion, bulk deletion, import, or copy-to-live.
170
-
171
- ## Before you finish
172
-
173
- ```bash
174
- cai schema get custom.<type> --json
175
- cai data collections --env <dev|live> --json
176
- cai data query --type custom.<type> --env <dev|live> --json
177
- cai workflow outline --json
178
- cai workflow validate --json
179
- ```
57
+ Development is the default test store; MCP writes require explicit `env`. Publishing the workflow does not copy records to Live. Use explicit validated copy/import operations when data promotion is actually intended. Verify state in the same environment as the flow execution.
180
58
 
181
- - Schema read-back must show the intended stable key, display, complete field list, and field types.
182
- - The same-rail query must prove the expected persisted records; node output alone is insufficient.
183
- - Compare schema and workflow inventory with the request. Remove only schemas created during abandoned attempts, never pre-existing user objects.
184
- - For a live request, verify the live collection after publish or copy and report the affected record count.
59
+ Detailed schema, import and data commands: [command reference](references/command-reference.md). Environment and reference semantics: [data rails](references/data-rails.md).
@@ -0,0 +1,177 @@
1
+ # Controller AI data
2
+
3
+
4
+ Controller AI has two separate concerns:
5
+
6
+ - A `custom.*` schema defines fields and makes a collection available.
7
+ - Development and live record stores hold separate rows. Publishing graph/configuration does not copy records.
8
+
9
+
10
+ - Persisting data requires both a schema and a create-record node; see A type vs a stored record.
11
+ - `schema update --field` replaces the full field list; see Define a schema deliberately.
12
+ - Relabel a stable key instead of recreating it; see How schema changes affect stored records.
13
+ - Query results and writes use different field keys; see Respect field-key asymmetry.
14
+ - Read and write the requested dev or live store; see Work on the correct rail.
15
+ - Remove only scratch schemas created for abandoned attempts; see Before you finish.
16
+
17
+ ## A type vs a stored record
18
+
19
+ When a request says store, save, log, or record with no external destination, create the schema and configure a create-record node. A flow output alone is not persistence.
20
+
21
+ `custom.customer` and `dataRecord.custom.customer` are different things, and the difference carries through everything:
22
+
23
+ - `custom.customer` is the TYPE — the shape, like a TypeScript interface. A value of this type is just data in that shape.
24
+ - `dataRecord.custom.customer` is an ACTUAL STORED RECORD of that shape — the same fields plus record identity (`id`, `createdAt`, `updatedAt`) proving it lives in the collection.
25
+
26
+ In the product UI this appears as the "data record" checkbox when selecting a type for a flow input or run-subflow-on-list: unchecked means "data shaped like a customer", checked means "a real stored customer record". Users find this checkbox confusing — be ready to explain it in these terms.
27
+
28
+ Consequences:
29
+
30
+ - Only real records satisfy a `dataRecord.*` input. An expression that CONSTRUCTS record-shaped objects (e.g. a `.map()` that builds new objects) produces `custom.*`-shaped values with no identity — pass fetched records through instead.
31
+ - Record metadata (`id`, `createdAt`) exists only on `dataRecord.*` values; on plain `custom.*` values those paths resolve empty.
32
+
33
+ ## Data nodes: update semantics
34
+
35
+ - "Update multiple data records" takes an expression typed `list.dataRecord.custom.<type>` — real stored records passed through from a query or `workflowData(...)`. The batch is ONE transaction: if any resolved record is stale (deleted since fetched) or foreign to the collection, the node errors and NOTHING updates. A failed run therefore means zero changes — fix and rerun safely.
36
+ - On the single-record "Update data" node, every ENABLED field is written. Enable only the fields you intend to change; disabled fields keep their stored values.
37
+
38
+ ## Define a schema deliberately
39
+
40
+ Inspect existing types first:
41
+
42
+ ```bash
43
+ cai schema list --json
44
+ cai schema get custom.<name> --json
45
+ ```
46
+
47
+ Create a type with stable field IDs:
48
+
49
+ ```bash
50
+ cai schema create custom.customer \
51
+ --display "Customer" \
52
+ --field email:text:"Email" \
53
+ --field score:number:"Score" \
54
+ --json
55
+ ```
56
+
57
+ Creating the schema creates/synchronizes its collection. `cai data collection create` is only an alias for schema creation.
58
+
59
+ MCP: `cai data collection create` is merged into `schema_type_create`.
60
+
61
+ `schema update --field` replaces the complete field list; it does not append — this is deliberate, so a subset CAN intentionally replace the whole schema. Read the current schema, preserve intended fields, and disclose any removals. The transport response warns about every field being dropped and echoes back the COMPLETE stored shape after the write — always read that echo and confirm it matches intent. Deleting a schema hides its collection and records and breaks dependent expressions — tell the user, and know it is recoverable (see below).
62
+
63
+ ## How schema changes affect stored records
64
+
65
+ Schema changes never hard-delete data. There is no delete path for records in schema sync — everything is deactivation and hiding:
66
+
67
+ - Delete a type → its collection goes INACTIVE on the next saved state; every record row survives. Re-add the same `custom.*` key and the same collection reactivates with all records intact.
68
+ - Delete a field → its values stay inside each record, unaddressable. Re-add the SAME field id and the old values reappear.
69
+ - Add a field → purely additive; existing records read the new field as null.
70
+ - Change a field's type → values are never converted or erased; incompatible values read as null and return if the type is changed back. The schema hash changes, which blocks copy-to-live until dev and live match again.
71
+ - Publish applies the same deactivate/reactivate logic to live. Live records are hidden, never deleted.
72
+ - Restoring an older workflow state rolls back the DEFINITION (graph and schemas), never the data. The record store is not versioned and only moves forward: a deleted type reactivates with whatever the store holds NOW — records created, edited, or soft-deleted after the restored state stay exactly as they are. Restore is not a data snapshot.
73
+
74
+ The truly destructive operations are record-level only: `data delete`, `data delete-many`, and overwriting edits. Those have no undo — treat them with explicit user intent; everything schema-shaped is safely reversible.
75
+
76
+ Relabel a type without changing its stable key:
77
+
78
+ ```bash
79
+ cai schema update custom.customer --display "Customers" --json
80
+ ```
81
+
82
+ Changing `custom.customer` itself is a key migration, not a relabel. It creates a new type and deactivates the old collection, stranding its records under the old key. Migrate records explicitly by export/import or restore the old key.
83
+
84
+ ## Respect field-key asymmetry
85
+
86
+ ```bash
87
+ cai data fields --type custom.customer --json
88
+ ```
89
+
90
+ - `data create` and `data update` take field IDs.
91
+ - Query/get results are keyed by field display names.
92
+ - CSV columns are also matched by display names.
93
+
94
+ Never feed a queried record directly into a write. Build a new fields object keyed by the `writeKey` mapping and omit reserved `id`, `createdAt`, and `updatedAt`.
95
+
96
+ Fields may be typed `date`. Their values are strict ISO-8601 strings (`"2026-06-05"` or `"2026-06-05T12:30:00Z"`) on both write and read; anything else is rejected. Reserved `createdAt`/`updatedAt` are currently `text` — use `.toDate()` before date operations on them.
97
+
98
+ ## Work on the correct rail
99
+
100
+ Development is the default:
101
+
102
+ ```bash
103
+ cai data collections --env dev --json
104
+ cai data query --type custom.customer --env dev --json
105
+ cai data create --type custom.customer --fields '{"email":"a@example.com","score":5}' --env dev --json
106
+ ```
107
+
108
+ MCP: every data write requires explicit `env`; reads default to `dev` when `env` is omitted.
109
+
110
+ Use `--env live` only when the user intends to inspect or mutate production state. Before a live write, say that published/triggered runs read this store.
111
+
112
+ `data query --query` is plain-text search over serialized records and metadata, not a field predicate. Perform exact field filtering inside workflow expressions or inspect results explicitly.
113
+
114
+ ## Delete records explicitly
115
+
116
+ Native workflow Delete Record actions provide reusable deletion inside a flow. Discover their exact keys under registry_data. The data tools below also delete one known record or an explicit set of IDs during setup and debugging:
117
+
118
+ ```bash
119
+ cai data delete <recordId> --type custom.customer --env dev --json
120
+ cai data delete-many --type custom.customer --records <id1,id2> --env dev --json
121
+ ```
122
+
123
+ Delete every record in a collection only with explicit user intent; `--all` also requires `--yes`:
124
+
125
+ ```bash
126
+ cai data delete-many --type custom.customer --all --yes --env dev --json
127
+ ```
128
+
129
+ MCP: `data_record_delete_many` requires explicit `env: dev|live` and exactly one of `recordIds` or `all: true`; it has no `yes` parameter.
130
+
131
+ The CLI default rail is development. Targeted and `--all --yes` bulk deletions also support `--env live`; a live deletion immediately changes the data read by the published workflow, so use it only with explicit production intent and query that rail afterward.
132
+
133
+ ## Import and promote
134
+
135
+ Validate CSV before writing:
136
+
137
+ ```bash
138
+ cai data import --type custom.customer --file customers.csv --mode dryRun --env dev --json
139
+ cai data import --type custom.customer --file customers.csv --mode commit --env dev --json
140
+ ```
141
+
142
+ MCP: pass CSV text inline as `content` with explicit `env: dev|live` to `data_record_import_validate`, then use `data_record_import_commit` to write the validated content.
143
+
144
+ Both CLI modes support `--env live`: dry-run remains non-writing, while a live commit appends rows immediately visible to the published workflow. Verify result counts and query the imported rows on the same rail.
145
+
146
+ To seed live from development:
147
+
148
+ ```bash
149
+ cai workflow publish --json
150
+ cai data copy-to-live --type custom.customer --json
151
+ cai data collections --env live --json
152
+ ```
153
+
154
+ MCP: call `data_copy_to_live` with the explicit acknowledgement `env: live`.
155
+
156
+ The live schema must exist through a publish before copying. Treat copy-to-live as a production mutation and report counts/results.
157
+
158
+ ## Workflow expressions
159
+
160
+ `workflowData("custom.customer")` requires an active collection on the executing rail. Read `controllerai-expressions` for filtering. Test stateful behavior against development, then independently verify required live seed data.
161
+
162
+ Read [data-rails.md](data-rails.md) before schema rewrites, deletion, bulk deletion, import, or copy-to-live.
163
+
164
+ ## Before you finish
165
+
166
+ ```bash
167
+ cai schema get custom.<type> --json
168
+ cai data collections --env <dev|live> --json
169
+ cai data query --type custom.<type> --env <dev|live> --json
170
+ cai workflow outline --json
171
+ cai workflow validate --json
172
+ ```
173
+
174
+ - Schema read-back must show the intended stable key, display, complete field list, and field types.
175
+ - The same-rail query must prove the expected persisted records; node output alone is insufficient.
176
+ - Compare schema and workflow inventory with the request. Remove only schemas created during abandoned attempts, never pre-existing user objects.
177
+ - For a live request, verify the live collection after publish or copy and report the affected record count.
@@ -1,173 +1,51 @@
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: Write and debug Controller AI typed expressions for flow inputs, upstream outputs, records, conditions and calculations. Use for data mapping, search, filtering, formatting, optional inputs and computed node props.
4
4
  ---
5
5
 
6
- # Controller AI expressions
6
+ # Expressions: typed values inside a workflow
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
+ Expressions connect workflow data to node properties and conditions. The JavaScript-looking authoring language compiles to Controller AI's stored expression tree; it is a **typed subset**, not arbitrary JavaScript execution. You can navigate inputs and fields, calculate numbers, format text and dates, query workflow data and operate on lists.
9
9
 
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.
10
+ ## Discover the real scope
11
11
 
12
- ## Rules that break tasks when skipped
12
+ `expr_operation_list` provides the method catalog, including before a workflow has nodes. `expr_context` for a node shows current flow/input bindings, available fields, upstream outputs not yet wired, and the target property's expected type. Use returned names; display labels and stored IDs are not interchangeable.
13
13
 
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.
14
+ Common sources are `flow.<binding>`, `inputs.<binding>`, `process.data`, loop `item`, `now`, `isLiveVersion` and `workflowData("custom.type")`. Upstream values appear under `inputs` only after the relevant output is wired in. Root routing nodes can use flow inputs directly.
20
15
 
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.
16
+ ## Compose values
22
17
 
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.
24
-
25
- ## How empty values evaluate
26
-
27
- An empty input evaluates to its typed empty form: `""` for TEXT, null/undefined for objects. Both cases below fail silently:
28
-
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.
31
-
32
- When a run unexpectedly returns zero results, check the resolved configs for empty comparison values first (`cai exec node <nodeExecutionId> --full`).
33
-
34
- ## Required write loop
35
-
36
- ### 1. Wire first
37
-
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.
39
-
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.
45
-
46
- ### 2. Read the destination's exact scope
47
-
48
- ```bash
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
- cai expr ops --workflow <workflowId> --type <text|number|date|list|boolean|file|custom> --json
70
- ```
71
-
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`.
73
-
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:
89
-
90
- ```bash
91
- cai expr set <nodeId> --prop <propName> --js '<expression>' --json
92
- ```
93
-
94
- MCP: use `node_prop_set` with `js`; it merges `cai expr set` with `cai node set`.
95
-
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`.
97
-
98
- ```bash
99
- cai expr set <nodeId> --prop <propName> --js-file expression.js --json
18
+ ```javascript
19
+ flow.name.toLowerCase()
20
+ `Hello ${flow.name}, score ${flow.score}`
21
+ flow.score + 10
22
+ flow.label ?? "Unknown"
23
+ flow.enabled ? "Enabled" : "Disabled"
24
+ workflowData("custom.contact").filter(r => r.id === flow.recordId).first()
100
25
  ```
101
26
 
102
- MCP: pass the file contents inline as `js`; the hosted process does not read the local path or stdin.
103
-
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>`.
27
+ Use field IDs with the provided dot/bracket spelling. Prefer templates for text composed from multiple values. Dates use the supported date methods and Moment format tokens, for example `now.format("YYYY-MM-DD")`; do not assume JavaScript Date constructors/getters exist.
105
28
 
106
- MCP: repair with `node_optional_prop_set` and `enabled: false`.
29
+ A record query returns `list.dataRecord.custom.*`, including record identity. Plain custom values describe shape only. Optional filter semantics, membership and pagination are explained in `controllerai-data`.
107
30
 
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.
31
+ ## Conditions and list predicates
109
32
 
110
- For a side-effect-free check:
33
+ A single comparison or supported boolean expression can be a condition. Router/only-when groups support OR-of-AND groups through the structured condition input. Use that input for compound routing logic instead of assuming every JavaScript boolean expression is accepted everywhere.
111
34
 
112
- ```bash
113
- cai expr validate <nodeId> --prop <propName> --js '<expression>' --json
114
- ```
35
+ List `.filter`, `.some`, `.find` and `.every` accept the predicate grammar in the catalog. `.map` is field projection, not an arbitrary object-building callback. List membership compares an element; text membership finds a case-sensitive substring.
115
36
 
116
- For conditions:
37
+ Scalar text/number/boolean ternaries are supported; list, date and record branches need typed routing or a field-mask update instead. Do not assume methods on literals, arbitrary null literals, JavaScript constructors or object-building callbacks are accepted.
117
38
 
118
- ```bash
119
- cai expr validate <nodeId> --mode onlyWhen --js '<boolean expression>' --json
120
- 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
126
- cai branch update <nodeId> --route <routeId> --condition-json '[["flow.email.isNotEmpty()","flow.units < 5"],["flow.override === true"]]' --json
127
- ```
39
+ If the compiler rejects an idiom, use the error's supported alternative and validate it. Do not invent raw expression trees to bypass a missing language feature. Use routing, typed inputs or a native action where they express the operation more clearly.
128
40
 
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`.
41
+ ## Save and verify
130
42
 
131
- Router and If/Else conditions are written with `cai branch add` or `cai branch update` after the same context/validation discipline.
43
+ 1. Inspect the property and its expected type.
44
+ 2. Validate the proposed expression with `expr_validate` against the node scope.
45
+ 3. Persist with `node_prop_set` using `js` (or the relevant condition tool). Literal `value` and compiled `js` are different sources.
46
+ 4. Read the stored property and resolved status; run representative inputs.
47
+ 5. Check resolved configs and actual results, including omission, `false`, `0`, empty text/list and missing record cases relevant to the task.
132
48
 
133
- ### 5. Verify runtime resolution
134
-
135
- ```bash
136
- cai expr decompile <nodeId> --prop <propName> --json
137
- 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
- cai run flow <flowId> --inputs <values.json> --json
146
- ```
147
-
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.
151
-
152
- ## Hard rules
153
-
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.
161
-
162
- ## Before you finish
163
-
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
- ```
49
+ A successful compile proves grammar and type compatibility, not that a query finds data. An intentional empty result can be valid; an unexpected empty value needs tracing to the binding, environment or filter.
170
50
 
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`.
51
+ Reference syntax and mapping commands: [command reference](references/command-reference.md). Supported grammar and recipes: [DSL reference](references/dsl-and-recipes.md).