@lotics/cli 0.315.18 → 0.316.1

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.
@@ -1,22 +1,21 @@
1
- # App bindings — the queries, workflows and agents an app calls
1
+ # App bindings — the queries and workflows an app calls
2
2
 
3
- A custom-code app reaches workspace data through three kinds of binding, each declared on the app
3
+ A custom-code app reaches workspace data through two kinds of binding, each declared on the app
4
4
  under an alias — a JS identifier (`/^[a-zA-Z_$][a-zA-Z0-9_$]*$/`):
5
5
 
6
6
  | Binding | Declared with | The app calls it with | An agent calls it with |
7
7
  |---|---|---|---|
8
8
  | Query | `set_app_queries` (CLI, chat) | `useQuery("<alias>", params?)` | `run_app_query` |
9
9
  | Workflow | `set_app_workflow` | `useWorkflow("<alias>")({ ...inputs })` | `run_app_workflow` |
10
- | Agent | `set_app_agent` (CLI, chat) | `useAgentRun("<alias>")({ ...inputs })` | — |
11
10
 
12
11
  Each runs under the app's authority (`{ type: "app", app_id }`), the app owner's reach — never the
13
12
  calling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next
14
- call uses it. `get_app_capabilities` lists an app's aliases with their params and inputs.
13
+ call uses it. `get_app` lists an app's aliases with their params and inputs.
15
14
 
16
15
  Each reader returns a fingerprint of what it read — `get_app_workflow`'s `body_sha`, and on the CLI and in chat
17
- `get_app_query`'s `sha` and `get_app_agent`'s `instructions_sha` — and a write built on that read passes it back:
16
+ `get_app_query`'s `sha` — and a write built on that read passes it back:
18
17
  `set_app_queries`' `expected_shas` (alias → `sha`), `set_app_workflow`'s `expected_body_sha`,
19
- `set_app_agent`'s `expected_instructions_sha`, `remove_app_binding`'s `expected_sha`. The write is
18
+ `remove_app_binding`'s `expected_sha`. The write is
20
19
  refused, and writes nothing, when the binding changed since; omit it for an unconditional write.
21
20
 
22
21
  ## Queries
@@ -66,9 +65,15 @@ Filters (`from_table.filter`, `filter.predicate`, an aggregate's `filter`) are t
66
65
  `filters` reference — over the input's columns, taking `{{params.x}}` as values. `search` matches
67
66
  every text-bearing field of the row, accent- and case-insensitive.
68
67
 
69
- A `project` column is a field key as a bare string (output name and type come from the field), or
68
+ A query names a table's fields by the alias its document states, wherever a node reads the table directly (a
69
+ `from_table`'s filter and sort, the `project`, `filter`, `group`, `window`, `unnest` or `unpivot` over it) and a select
70
+ condition's values by the option's alias. A column a `project` writes answers under the alias it was written with; every
71
+ name past a `project` or an aggregate is that column's own. A `union` whose arms read one alias from different tables
72
+ projects each arm's columns under one name first. The query is stored by key and read back by alias.
73
+
74
+ A `project` column is a field's alias as a bare string (output name and type come from the field), or
70
75
  `{ output?, type?, source, writable_target?, limit? }` to rename, compute or bound one. A computed
71
- `source` is a field key, `{ literal }`, `{ eq: [a, b] }`, `{ neq: [a, b] }`, `{ isEmpty }`,
76
+ `source` is a field's alias, `{ literal }`, `{ eq: [a, b] }`, `{ neq: [a, b] }`, `{ isEmpty }`,
72
77
  `{ isNotEmpty }`, `{ coalesce: [...] }`, `{ concat: [...] }`, `{ record_id: true }`,
73
78
  `{ link: { source, field } }` (a field of the first linked record),
74
79
  `{ link_agg: { source, field, operation } }` (`sum`, `avg`, `min`, `max`, `count`, `string_agg`
@@ -92,7 +97,7 @@ name → `{ type, required?, description?, …per type }`. An entry is required
92
97
  |---|---|
93
98
  | `text`, `number`, `boolean`, `date`, `datetime`, `email` | — |
94
99
  | `record_link` | `table_id`; `multi?`; `max?` (with `multi`, the most ids one call carries) |
95
- | `select` | exactly one of `options: [{ label, value }]` or `field: "fld_…"` (a select field whose CURRENT options back it); `multi?` |
100
+ | `select` | exactly one of `options: [{ label, value }]` or `field: "<table id>.<alias>"` (a select field whose CURRENT options back it); `multi?` |
96
101
  | `date_range` | `include_time?` |
97
102
  | `member` | `multi?`; `group?` (a member group the value must belong to) |
98
103
  | `file` | `multi?` — an uploaded `fil_…` id, for a files field |
@@ -113,7 +118,7 @@ name → `{ type, required?, description?, …per type }`, every entry present u
113
118
  |---|---|
114
119
  | `text`, `number`, `boolean`, `date`, `datetime`, `email`, `json` | — |
115
120
  | `record_link` | `table_id`, `multi?` |
116
- | `select` | exactly one of `options: [{ label, value }]` or `field: "fld_…"`; `multi?` |
121
+ | `select` | exactly one of `options: [{ label, value }]` or `field: "<table id>.<alias>"`; `multi?` |
117
122
  | `object` | `fields` |
118
123
  | `array` | `items` |
119
124
 
@@ -124,12 +129,13 @@ keeps the type tracking the live field.
124
129
  ## Workflows
125
130
 
126
131
  `set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,
127
- name?, description? }`. On an app built from a model, the alias is one the model names — an act's own
128
- `workflow`, or an entry of the app's `workflows`, which states `inputs`, `outputs` and `description`:
129
- a call stating any of them for it is refused, and outputs the entry leaves out are derived from the
130
- body. An alias the model names nowhere is archived by the next apply. An entry of `workflows` runs
131
- only for someone who holds the app: on an app shared by a public link, a visitor's call of it is
132
- refused as an alias the app does not have.
132
+ name?, description? }`. On an app written as a tree the alias is one the tree names — a Workflow node's
133
+ `key` or an Act's `workflow` — and the node states `inputs`, `outputs` and `description`
134
+ (`model/workflow-node`, whose value shapes differ from the **Inputs** below): a call stating any of
135
+ them for it is refused, and outputs the node leaves out are derived from the body. An alias the tree
136
+ names nowhere is archived by the next `set_app`. A Workflow node runs only for someone who holds the
137
+ app: on an app shared by a public link, a visitor's call of it is refused as an alias the app does
138
+ not have. This page's `inputs` and `outputs` are for a custom-code app.
133
139
 
134
140
  - `source` is the body with no `on({...})` trigger — the app is the trigger. It reads each declared
135
141
  input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id
@@ -145,50 +151,6 @@ refused as an alias the app does not have.
145
151
  and writes nothing. A call naming a live alias replaces its body in place, keeping its run history.
146
152
  - A call whose payload does not match `inputs` is refused before the body runs.
147
153
 
148
- `get_app_workflow` reads the body back; `dry_run_workflow` with `trigger_type: "app_workflow"`
149
- runs it against sample inputs before binding; on the CLI and in chat, `remove_app_binding` with
154
+ `get_app_workflow` reads the body back; `set_app_workflow` with `verify_only` and `record_id` or `preview_inputs`
155
+ rehearses a draft before binding; on the CLI and in chat, `remove_app_binding` with
150
156
  `kind: "workflow"` unbinds it.
151
-
152
- ## Agents
153
-
154
- On the CLI and in chat, `set_app_agent` binds a streaming tool-loop agent to an alias. A run streams its work to the app,
155
- returns a typed result and keeps a run history per session.
156
-
157
- The declaration:
158
-
159
- - `instructions` — the task, every run. A new alias needs it.
160
- - `tool_names` — the tools it may call, from these and no other: `analyze_pdf_template`,
161
- `code_edit_file`, `code_exec`, `code_read_file`, `code_write_file`, `excel_create_file`,
162
- `excel_find_cells`, `excel_format_range`, `excel_get_range`, `excel_update_range`,
163
- `generate_bank_qr_code`, `generate_document`, `generate_image`, `generate_qr_code`, `get_template`,
164
- `grep_knowledge`, `list_banks`, `list_knowledge`, `lookup_business`, `query_templates`,
165
- `read_knowledge`, `run_app_query`, `run_app_workflow`, `validate_excel_template`,
166
- `vietcombank_convert_currency`, `view_files`, `word_create_document`, `word_find_text`,
167
- `word_get_content`, `word_get_table_data`, `word_insert_conditional`, `word_insert_loop`,
168
- `word_replace_text`. `[]` is a reasoning-only agent. A few fields of a document read well through
169
- the `excel_*` / `word_*` tools; a cross-check, reconciliation or transform a decision rests on
170
- belongs in the code tools.
171
- - `query_aliases` / `workflow_aliases` — the app's own queries and workflows it may call through
172
- `run_app_query` / `run_app_workflow`. They are its whole reach over records: no agent tool takes a
173
- `table_id`, so a read is bounded by the query's tables, rows and columns, and a write goes through
174
- the app's own workflow.
175
- - `knowledge_doc_ids` — knowledge docs it may read with `grep_knowledge` / `read_knowledge`
176
- (declare them in `tool_names`). Each must be one the app owner and you can use. Nothing is
177
- inlined and size does not matter. The list is the agent's whole reach — `list_knowledge` finds no
178
- doc outside it — so name a doc's id in `instructions` to point it there. `code_exec` computes
179
- across a corpus (counting, cross-referencing); reading needs only the knowledge tools.
180
- - `model_tier` — `haiku`, `sonnet` or `opus`. Omitted, the run follows the platform's default tier
181
- and moves with new models; pin one only as a tested choice, and never `haiku`, a utility tier.
182
- - `effort_level` — `low`, `medium`, `high`, `xhigh` or `max`, one the pinned tier supports; it
183
- needs `model_tier`.
184
- - `prefix_cache_ttl` — `5m` (the default) or `1h`. `1h` doubles the cache write price and pays only
185
- when runs land 5 to 60 minutes apart, as a scheduled sweep does.
186
- - `inputs` — the per-run payload (see **Inputs**); omitted, the payload is untyped.
187
- - `outputs` — the structured result (see **Outputs**), checked before the run is saved and typed
188
- as `run.output` in the app; omitted, the result is the final message.
189
- - `writes` — where the outputs land: `{ table_id, row, fields }`, `row` naming a single
190
- `record_link` input to `table_id` and `fields` mapping each output name to a field key of that
191
- table. A result that validates is written to that row under the app's authority.
192
-
193
- Send only what you change: an omitted field keeps its stored value, `null` clears an optional one.
194
- On the CLI and in chat, `get_app_agent` reads one back; `remove_app_binding` with `kind: "agent"` deletes it.
@@ -1,33 +1,34 @@
1
1
  # Building an app, end to end
2
2
 
3
- The other references here describe **contracts** — what a model may state, what a tool takes. This
3
+ The other references here describe **grammars** — what a table document or an app tree may state, what a tool takes. This
4
4
  one describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then
5
5
  reach for the area doc (`lotics docs`) whenever you need the detail.
6
6
 
7
- **The live app is the only edit surface.** Every change to an app — applying a model, deploying a
8
- build, setting one query or workflow — mints a new version of it, and rolling back to an earlier
9
- version is the undo. Nothing about an app lives in a local directory the platform reads back.
7
+ **The live app is the only edit surface.** Every change to an app — applying a tree, deploying a
8
+ build, setting one query or workflow — mints a new version of it. To go back, apply the earlier tree
9
+ again (or deploy the earlier source). Nothing about an app lives in a local directory the platform
10
+ reads back.
10
11
 
11
- **Rolling back restores the app, not the data.** A table change the model made, and every row a
12
- workflow wrote while you tried it, stay where they are. Try a write on a throwaway record.
12
+ **Going back restores the app, not the data.** A table change, and every row a workflow wrote while
13
+ you tried it, stay where they are. Try a write on a throwaway record.
13
14
 
14
15
  ---
15
16
 
16
17
  ## 1 — Two kinds of app
17
18
 
18
- - **An app stated in a model** — the default, and the right one for almost every job. `model.json`
19
- says how a row of each entity is recognised (`records`) and, in `apps`, one working register over
20
- one entity and the record its rows open, with the logs that stand beside it (`logs`, read by the organization's own members alone): its columns and filters, its sections in the order its work
21
- reaches them, its acts and the checks that guard them (`lotics docs model/register`,
22
- `model/record-page`, `model/acts`, `model/checks`). The platform
19
+ - **An app stated as a tree** — the default, and the right one for almost every job. The tree (a
20
+ JSON file, one per app) is a List of one table, whose rows are recognised by the
21
+ table's own `records`, and the Record its rows open, with the logs that stand beside it (further Lists at the root, which a change may add at any time, read by the organization's own members alone): its columns and filters, its Sections in the order its work
22
+ reaches them, its Acts and the Checks that guard them (`lotics docs model/app-tree`,
23
+ `model/list-node`, `model/section-node`, `model/act-node`, `model/check-node`). The platform
23
24
  compiles each app and draws it with the runtime every such app shares, so how each piece looks is
24
- the platform's, one way per concept, and no key changes it. Every write the app makes is a
25
- generated workflow that re-checks on the server what the model states.
26
- - **A custom-code app** — React you write, for a surface the model's vocabulary cannot state. It
25
+ the platform's, one way per concept, and no prop changes it. Every write the app makes is a
26
+ generated workflow that re-checks on the server what the tables state.
27
+ - **A custom-code app** — React you write, for a surface the tree's components cannot state. It
27
28
  reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever
28
29
  React you choose.
29
30
 
30
- ## 2 — Clarify what is being asked, before modelling it
31
+ ## 2 — Clarify what is being asked, before designing it
31
32
 
32
33
  **A metric name is not a definition.** "Revenue", "in stock", "active", "overdue" — each is a
33
34
  business rule the person asking owns, and the cost of guessing is a screen that is confidently
@@ -46,76 +47,93 @@ invisible in review: every later artifact is correct with respect to the wrong d
46
47
 
47
48
  ## 3 — The data model, before any app
48
49
 
49
- Get this wrong and nothing above it can be precise. Each entity is its own table with links into
50
+ Get this wrong and nothing above it can be precise. Each kind of thing is its own table with links into
50
51
  the spine; attributes and evidence are fields on their owner. A single table with a `type` column
51
- standing in for three entities collapses the distinctions every later query needs.
52
+ standing in for three kinds of thing collapses the distinctions every later query needs.
52
53
 
53
54
  **Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and
54
55
  look at fill rates — a field that is present and empty on 90% of rows will not support the screen
55
56
  you are about to design.
56
57
 
57
- **That includes imagery.** If the entity has a likeness — a product, a property, a vehicle, a
58
+ **That includes imagery.** If the thing has a likeness — a product, a property, a vehicle, a
58
59
  person — its picture is the strongest identifier a register row can carry, and an empty image field
59
60
  is a data gap to fill before you design around it: `lotics file upload`, then `update_records`.
60
61
 
61
- **One fact, one column — and check before you add one.** Read the table's existing fields before
62
- adding any, because the fact is often already there in another shape: a place written as text
63
- beside a link to the place record, a status word beside the select that decides it, a total beside
64
- the formula that computes it. Two columns for one fact never stay equal. Prefer the link, the
65
- select, or the formula, and compose the text when you READ.
66
-
67
- **Match on ids and option keys, never on rendered text.** Resolve a name to its `rec_…` or `opt_…`
62
+ **Match on ids and option aliases, never on rendered text.** Resolve a name to its `rec_…` or its option alias
68
63
  once, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.
69
64
 
70
- **How the tables RELATE is `lotics docs data_model`** — read it before designing a schema; those
71
- decisions outlive any one app.
65
+ **How the tables RELATE, and one fact in one column, is `lotics docs data_model`** — read it before designing a
66
+ schema; those decisions outlive any one app.
72
67
 
73
- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a real
74
- distinction. Read what a field MEANS before you remove it.
68
+ ## 4 — An app stated as a tree
75
69
 
76
- ## 4 — An app stated in a model
70
+ Tables first, then apps. `set_tables` takes a list of table documents, one or many, and writes them all or
71
+ none; `set_app` takes one app per call. Each creates when it is given no id and edits when it is given one.
77
72
 
78
73
  ```
79
- lotics docs model # how to write model.json, with a worked example
80
- lotics docs design # how to design each app: the method, a treatment per kind of row
81
- lotics model apply model.json # check it, apply the tables, mint a version of every app
82
- lotics model apply model.json --app orders # only the apps named; the tables are applied whole
83
- lotics model check model.json # what the apply would change, writing nothing
84
- lotics model pull -o model.json # the workspace's model, as the file apply reads
74
+ lotics docs model # the keys of a table document and the app tree's components, with a worked example
75
+ lotics docs design # the treatment per kind of row
76
+ lotics run set_tables @tables.json # one call, every table: {"tables": [...]}
77
+ lotics run set_app @orders.call.json # one app: {"body": <the App tree>} to create (add "dry_run": true to check)
78
+ lotics app apply orders.app.json --check # the same tool from a bare tree file: what would refuse it, writing nothing
79
+ lotics app apply orders.app.json # no app known: creates it; known: edits it to the file
80
+ lotics app pull <app_id> # the app's tree, as ./<name>.app.json
85
81
  ```
86
82
 
87
- `model apply` checks the whole file first — every problem in one run, before anything is uploaded
88
- or written. It then adopts or creates each table (the table this workspace bound it to, else one of the same
89
- label, is adopted and given what it lacks; no stored value changes), writes the first rows only where every bound
90
- table is empty, and mints one version per app, printing each app's id, the version minted
91
- (`unchanged` when there was nothing to mint) and its address. Applying the same file again mints nothing.
92
- A table change is not undone by rolling an app back, so `lotics model check` first says what the apply would
93
- create or change in the tables and which apps it would create, update or refuse — writing nothing.
94
-
95
- **An app leaving out a treatment its rows call for is refused** — readings over its rows, a record's
96
- sections, an act per status move, a picture where its rows hold photos (`lotics docs design`). Each
97
- refusal names the rule, and beside them comes each refused app's patch adopting its decisions: merge
98
- the patches into the file in the order given, or state why the app stays as it is under the
99
- `declines` key the refusal names (`lotics docs model/declines`), then apply again. `lotics model check` reads the
100
- decisions beside the workspace's rows, and with `--json` gives each app as the patches leave it, its
101
- `draft`.
102
-
103
- **The model changes after the app exists, and applying it again is how it lands.** Edit the file,
104
- apply it. An act whose write the model cannot say names its own `workflow`; that workflow's body is
83
+ A new workspace is built in one order: one `set_tables` call with the tables, then every `set_app`
84
+ call, then fixes. One call carries at most 50 tables, 200 first rows a table, and 2000 rows and 2000
85
+ documents in all; a workspace of more tables is built in further calls, each naming the tables already made by
86
+ their ids. The example files at `https://lotics.ai/presets/index.json` are read and their tables passed to
87
+ `set_tables`.
88
+
89
+ A table is a document: its `fields` are keyed by alias, and a link field names the table it
90
+ points at by id, or by the `ref` of another table of the same call, and states `multi`. A
91
+ two-way link states `pair` on one table, as `true` or as a free alias, and the partner is made on the other; a many-to-many link states both sides. A table is edited by an RFC 7396 merge patch of its document:
92
+ what the patch leaves alone is left alone and `null` deletes. `dry_run` checks the call and writes
93
+ nothing; a refusal lists everything wrong in it at once.
94
+
95
+ The App node is the app: its `name`, `description`, `icon` and `color` are props of it, and no other input of
96
+ `set_app` states them. It names the tables and templates it uses by their aliases (an entity is a table's alias, a `template` a template's alias);
97
+ `set_app` fills the `tables` and `templates` maps by id from them and `get_app` shows aliases (`lotics docs model/app-node`). A call that creates
98
+ is `{"body": <the App tree>}`; one that changes a part is `{"app_id": "app_…", "nodes": {"<address>": <patch>}}`,
99
+ each address a node's path of keys (`get_app` prints every one), a patch a JSON merge patch of that node's props
100
+ and `null` removing the node; the App's own props change through `body`. A call stating `base_version_id` — the
101
+ version you read — is refused when the app has moved on. The old body grammar (`register`, `record`, `acts`,
102
+ `edits`) is refused with a sentence pointing here.
103
+
104
+ A tree states what it means once: a statement equal to what the runtime draws anyway (a column the row already shows) is kept,
105
+ drawn once and noted, while a statement that conflicts is refused. A register List's `label` is the register's heading in the app's lists; its rows are named by the table's own label and singular.
106
+ `type` may be omitted, and a Section, Side or Page
107
+ without a `key` is keyed by its title's slug, written once and kept.
108
+
109
+ `app apply` with no app known creates one from the file, named by the tree's `name`, and records its id and
110
+ version in `<file>.lotics.json` (`app pull` writes the same file), so the next apply of that file edits that
111
+ app; `--app <app_id>` names it instead, and is refused for a file whose record names another app. A known app
112
+ is read first and only what the file changes is sent, as a JSON merge patch (RFC 7396): a key the file drops
113
+ is removed, a list is replaced whole, and what the file leaves alone is left alone. A file pulled at a
114
+ version the app has since left is refused, naming the version: pull the current tree into another path and
115
+ carry your edits over. Applying a tree the app already has mints nothing.
116
+ `--check` sends the same change as `dry_run`: what would refuse the tree, the notes on it, and the permissions the
117
+ write would grant and remove, writing nothing. Every write's answer states those permissions: the saves, adds,
118
+ Deletes and act writes the new tree grants and removes, from the previous tree to the new one.
119
+
120
+ **Nothing is written into the tree for you.** The answer notes a treatment the rows call for that the tree leaves out —
121
+ Readings over its rows, a record's Sections, an Act per status move, a picture where its rows hold photos
122
+ (`lotics docs design`) — and the tree states the ones the job needs.
123
+
124
+ **The tree changes after the app exists, and applying it again is how it lands.** Pull the tree, edit
125
+ the file, apply it; or send `nodes` for one part. An Act whose write the tree cannot say names its own `workflow`; that workflow's code is
105
126
  the live one, and `lotics run set_app_workflow` changes it. A job no press starts — an agent or a
106
- service handing the app a file or a list of rows — is a workflow the app declares in `workflows`
107
- (`lotics docs model/workflows`): the model states what it takes and hands back, `lotics run
108
- set_app_workflow` sets its body with `inputs` and `outputs` left out, and every apply keeps that
109
- body. A workflow set on the app that its model declares nowhere is archived by the next apply; a query set on it that its model declares nowhere (`queries`) is removed by the next apply, which says so in its plan and its answer. An act that reads papers runs an agent
110
- apply owns: made, replaced and removed with the act, and an agent of that alias apply did not make
111
- is refused, never rewritten.
112
-
113
- **What a person reads and does in the app, an agent reads and does through it.** The apply generates a
114
- query for every list, record, child block and figure the screen reads, and a workflow for every add, edit,
115
- removal and act — `lotics run get_app_capabilities` lists them, and `lotics docs agent_surface` names each
116
- family and how it is called. A question the screen never asks is a query the app declares in `queries`
117
- (`lotics docs model/queries`), beside its `workflows`. Both are part of the design: step 5 of
118
- `lotics docs design`.
127
+ service handing the app a file or a list of rows — is a Workflow node the app declares
128
+ (`lotics docs model/workflow-node`): the node states what it takes and hands back, and `lotics run
129
+ set_app_workflow` sets its code with `inputs` and `outputs` left out.
130
+
131
+ **What a person reads and does in the app, an agent reads and does through it.** Writing the tree generates a
132
+ query for every List, record, child List and figure the screen reads, and a workflow for every add, edit,
133
+ removal and act — `lotics run get_app` lists them, and `lotics docs agent_surface` names each
134
+ family and how it is called. A question the screen never asks is a Query node the app declares
135
+ (`lotics docs model/query-node`), beside its Workflow nodes. Both are part of the design: `lotics docs
136
+ agent_surface`, § Declaring more.
119
137
 
120
138
  ## 5 — A custom-code app
121
139
 
@@ -134,7 +152,7 @@ version. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy
134
152
  and carries every binding forward unchanged.
135
153
 
136
154
  **Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column
137
- (`{ "source": "fld_…", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with
155
+ (`{ "source": "amount", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with
138
156
  `is_current_member` **inside the query** — a member id passed from the client is chosen by the
139
157
  caller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.
140
158
 
@@ -147,12 +165,12 @@ that caller.
147
165
  Static checks prove parse, types and names — they evaluate nothing. Rehearse a write first:
148
166
 
149
167
  ```
150
- lotics run dry_run_workflow '{"trigger_type":"app_workflow","trigger_payload":{…},"live_reads":true}'
168
+ lotics run set_app_workflow '{"app_id":"app_…","alias":"…","source":"…","verify_only":true,"record_id":"rec_…"}'
151
169
  ```
152
170
 
153
- It walks the real step tree and returns the resolved plan plus expression and tool-input errors,
154
- dispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it
155
- every read returns a stub, and every data-gated branch takes the empty path.
171
+ It walks the draft through the same engine an Act's preview uses, under the app's authority: reads are real,
172
+ writes and sends are said and not made, and it returns what the run would answer plus the calls it would make.
173
+ Pass `preview_inputs` for the values of a workflow that takes inputs and no row.
156
174
 
157
175
  Then run it end to end, on a throwaway record:
158
176
 
@@ -184,17 +202,15 @@ where a table left columns off at that width, `shed` names them.
184
202
 
185
203
  The app is drawn live, as you, read-only: a screen that writes when it opens shows that write
186
204
  refused, and `errors` lists what failed on the page. A version that looks wrong is one
187
- `lotics run rollback_app` away from the one before.
205
+ `lotics app apply` away from the one before: apply the earlier tree again.
188
206
 
189
- For an app a model applied, `verdict` comes beside the images: what a check of the workspace's
190
- model finds of that app now, beside its rows — what the next apply would refuse it for and the notes
191
- on it, the `patch` adopting the decisions among them, and the app's body as the patch
192
- leaves it (`draft`). `verdict_unread` says why none was read.
207
+ For an app stated as a tree, `verdict` comes beside the images: what a check of that app finds now,
208
+ beside its rows — what the next write would refuse it for and the notes
209
+ on it. `verdict_unread` says why none was read.
193
210
 
194
- The app's `name`, `icon`, `theme` and `description` are keys of the app in the model, and each apply
195
- writes them onto the app; an `icon` or `description` the model leaves out is cleared, and an app
196
- stating no `theme` is refused (`app.theme`). `lotics run update_app` changes them on
197
- the app, and `lotics model pull` reads them back as the app holds them. The
211
+ The app's `name`, `icon`, `color` and `description` are props of the App node; an app
212
+ stating no `color` gets the platform's grey tile. `lotics run set_app` with the app's id and a `body` patch changes them
213
+ on the app, and `lotics app pull` reads the tree back as the app holds it. The
198
214
  `description` heads the capability listing the member's chat agent reads on **every** turn, so a
199
215
  standing process the app expects that agent to carry out belongs there and nowhere else.
200
216