@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.
- package/dist/api/client.js +32 -0
- package/dist/api/client.js.map +1 -1
- package/dist/commands/agent.js +101 -8
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/expr.js +3 -10
- package/dist/commands/expr.js.map +1 -1
- package/dist/commands/file.js +11 -1
- package/dist/commands/file.js.map +1 -1
- package/dist/commands/integration.js +8 -2
- package/dist/commands/integration.js.map +1 -1
- package/dist/commands/workflow.js +29 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/errors.js +5 -2
- package/dist/errors.js.map +1 -1
- package/package.json +1 -1
- package/skills/controllerai-agents/SKILL.md +25 -192
- package/skills/controllerai-agents/references/command-reference.md +205 -0
- package/skills/controllerai-ai-nodes/SKILL.md +11 -92
- package/skills/controllerai-ai-nodes/references/command-reference.md +98 -0
- package/skills/controllerai-data/SKILL.md +34 -159
- package/skills/controllerai-data/references/command-reference.md +177 -0
- package/skills/controllerai-expressions/SKILL.md +29 -151
- package/skills/controllerai-expressions/references/command-reference.md +166 -0
- package/skills/controllerai-expressions/references/dsl-and-recipes.md +1 -1
- package/skills/controllerai-flow-control/SKILL.md +21 -160
- package/skills/controllerai-flow-control/references/command-reference.md +171 -0
- package/skills/controllerai-integration-actions/SKILL.md +11 -112
- package/skills/controllerai-integration-actions/references/command-reference.md +118 -0
- package/skills/controllerai-node-config/SKILL.md +18 -178
- package/skills/controllerai-node-config/references/command-reference.md +186 -0
- package/skills/controllerai-runs/SKILL.md +25 -117
- package/skills/controllerai-runs/references/command-reference.md +113 -0
- package/skills/controllerai-start/SKILL.md +36 -67
- package/skills/controllerai-start/references/mcp-tool-map.md +6 -0
- package/skills/controllerai-triggers/SKILL.md +16 -210
- package/skills/controllerai-triggers/references/command-reference.md +215 -0
- package/skills/controllerai-workflows/SKILL.md +24 -104
- package/skills/controllerai-workflows/references/command-reference.md +118 -0
- package/skills/manifest.json +120 -60
|
@@ -1,184 +1,59 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: controllerai-data
|
|
3
|
-
description:
|
|
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
|
-
#
|
|
6
|
+
# Workflow data as an application backend
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
- Development and live record stores hold separate rows. Publishing graph/configuration does not copy records.
|
|
12
|
+
## What is available
|
|
14
13
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
21
|
+
## Recipe: CRM flows for an agent
|
|
25
22
|
|
|
26
|
-
|
|
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
|
-
|
|
30
|
+
## Search expressions
|
|
29
31
|
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
## Keys, schemas and environments
|
|
116
52
|
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
#
|
|
6
|
+
# Expressions: typed values inside a workflow
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
## Discover the real scope
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
## Compose values
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
## Conditions and list predicates
|
|
109
32
|
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
## Save and verify
|
|
130
42
|
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|