@lotics/cli 0.315.17 → 0.316.0

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.
@@ -6,19 +6,23 @@ reach them, over MCP and as `lotics run <tool>`:
6
6
 
7
7
  | Tool | Answers |
8
8
  |---|---|
9
- | `get_app_capabilities` | The app's queries and workflows by alias: each one's description, its params or inputs with their types and legal values, and under a query's line its output columns (`returns`) — beside what each table they name holds, in its owner's words |
9
+ | `get_app` | The app's `catalog`: its queries and workflows by alias, each one's description, its params or inputs with their types and legal values, and under a query's line its output columns (`returns`) — beside what each table they name holds, in its owner's words |
10
10
  | `run_app_query` | The rows, a count or the groups of one query — or of several in one call |
11
- | `run_app_workflow` | One write — or several in order — or a preview of it that writes nothing |
11
+ | `run_app_workflow` | One write — or several in order — or a dry run of it that writes nothing |
12
12
 
13
- An app stated in a model generates this surface from the model; the model declares what the screen never
14
- asks (§ Declaring more). An app's logs (`logs`) are part of it: a log's list, its rows by id, its record's
15
- blocks and its acts are queries and workflows of the same app, named by the log's entity as the register's
16
- are by its own — `get_app_capabilities` lists them in the one roster, and nothing of a log is another app's. What only a log reads or runs answers the organization's own members alone, never a caller through the app's public link. A custom-code app's surface is the queries and workflows bound on it
17
- (`lotics docs app_bindings`).
13
+ An app generates this surface from its tree; Query and Workflow nodes declare what the screen never
14
+ asks (§ Declaring more). An app's logs (its further Lists at the root, which a change may add at any time) are part of it: a log's list, its rows by id, its record's
15
+ Lists and its Acts are queries and workflows of the same app, named by the log's table as the register's
16
+ are by its own — `get_app` lists them in the one roster, and nothing of a log is another app's. What only a log reads or runs answers the organization's own members alone, never a caller through the app's public link. A custom-code app's surface is the queries and workflows bound on it
17
+ (`app_bindings`).
18
+
19
+ ## Finding what an app offers
20
+
21
+ `get_workspace` maps the workspace. Its `search` matches the keys and descriptions of the Query and Workflow nodes an app declares, and the app's line names the matching keys; it never matches the generated `act_`, `create_` or `update_` aliases. `get_app` then gives the contracts. An agent runs a declared node before it falls back to `aggregate_records`.
18
22
 
19
23
  ## What every app generates
20
24
 
21
- A name in angle brackets is the model's alias for that thing. `get_app_capabilities` lists the ones a given
25
+ A name in angle brackets is the app's alias for that thing; an `<entity>` is the alias of a table the app reads. `get_app` lists the ones a given
22
26
  app has.
23
27
 
24
28
  ### Queries
@@ -27,24 +31,27 @@ app has.
27
31
 
28
32
  | Called | What it says of itself | Takes | Generated for |
29
33
  |---|---|---|---|
30
- | `<entity>_list` | Every row of <entity> the reader may see. | `search`?; `<scope entity>_id` where the app states `scope`; `<filter>_is`? per `register.filters` field holding one value — a select, a one-row link, a member, a yes/no; `<filter>_from`? / `<filter>_to`? per date filter — none for a date holding a span; `<filter>_min`? / `<filter>_max`? per number filter — none over `books`, none for a looked-up link or member, none for a number read in each row's own unit or currency | every entity the app draws; the filter params on the app's own entity alone |
31
- | `<entity>_list_<tab>` | The <entity> rows of the tab "<tab>". | the list's; `from`? / `to`? on a tab dated `in` a period; `to` on a tab of rows `open` on a day | each of `register.tabs`; none over `books`, none for a tab whose period reads a date holding a span |
32
- | `<entity>_totals_<tab>` | The <child> rows added per <entity> row, by "<link>", over the period from `from` to `to`, dated by "<date>": . | `from`; `to` | each of `register.tabs` stating `totals` |
34
+ | `<entity>_list` | Every row of <entity> the reader may see. | `search`?; `<scope entity>_id` where the app states `scope`; `<filter>_is`? per List `filters` field holding one value — a select, a one-row link, a member, a yes/no; `<filter>_from`? / `<filter>_to`? per date filter — none for a date holding a span; `<filter>_min`? / `<filter>_max`? per number filter — none over Books, none for a looked-up link or member, none for a number read in each row's own unit or currency | every entity the app draws; the filter params on the app's own entity alone |
35
+ | `<entity>_list_<tab>` | The <entity> rows of the tab "<tab>". | the list's; `from`? / `to`? on a tab dated `in` a period; `to` on a tab of rows `open` on a day | each Tab of the register; none over Books, none for a Tab whose period reads a date holding a span |
36
+ | `<entity>_totals_<tab>` | The <child> rows added per <entity> row, by "<link>", over the period from `from` to `to`, dated by "<date>": . | `from`; `to` | each Tab of the register stating `totals` |
33
37
  | `<entity>_record` | One row of <entity> by its id. | `<entity>_id` | every entity the app draws |
34
- | `<child>_by_<link>` | The <child> rows under one <entity>, through "<link>". | `<entity>_id` | each block of child rows on the record; one narrowed by `where` or ordered by `sort` adds `_<field>_<option>` and `_sorted_<field>` to its name |
35
- | `<child>_by_<link>_days` | The <child> rows of the <entity> rows handed in whose "<day>" falls from one day to another. | `<entity>_ids`; `from`; `to` | `register.roster` |
38
+ | `<child>_by_<link>` | The <child> rows under one <entity>, through "<link>". | `<entity>_id` | each List of child rows on the record; one narrowed by `where` or ordered by `sort` adds `_<field>_<option>` and `_sorted_<field>` to its name |
39
+ | `outside_<key>_one` | One row of <entity> by its id. | `record` | each Outside: the record a link opens, projected to the fields the Outside states |
40
+ | `outside_<key>_<child>` | The <child> rows under one <entity>, through "<link>". | `record` | each List of an Outside: the rows under the record a link opens, projected to the List's columns |
41
+ | `<child>_by_<link>_days` | The <child> rows of the <entity> rows handed in whose "<day>" falls from one day to another. | `<entity>_ids`; `from`; `to` | a roster List: the List of its cells |
36
42
  | `<entity>_<link>_pick` | The <target> rows "<link>" may point at, narrowed by what the reader typed. | `search`?; `same_<link>` per `same` link | a written link a write rule narrows (`options_where`, `same`) |
37
- | `<child>_by_<link>_picks` | The <child> rows no <entity> holds through "<link>" yet, that one <entity> may pick, narrowed by what the reader typed. | `search`?; `same_<link>` per `same` link | a block that picks existing rows into the record |
38
- | `register_reading_<n>` | <label>: how many <entity> rows there are. Each row carries `__count`. | the list's; `from`? / `to`? where the reading states `over`, or its `tab` is dated `in` a period; `to` where its `tab` is of rows `open` on a day | each of `register.readings` by its place among them, counted from 0 — but the work against its plan (`progress`), which reads the list's own rows; none over `books`. A reading stating `alias` is called by it |
39
- | `dashboard_reading_<n>` | <label>: how many <entity> rows there are. Each row carries `__count`. | `from`? / `to`? where the reading states `over` | each of a dashboard's readings, counted from 0; a reading stating `alias` is called by it |
40
- | `block_reading_<n>` | <label>: how many <child> rows there are. Each row carries `__count`. | `<entity>_id`; `same_<field>` where it is a list stating `same` | each reading, list or funnel block of the record, by its place among the record's blocks, counted from 0; a reading stating `alias` is called by it |
41
- | `<entity>_register_reading_<n>` | <label>: how many <entity> rows there are. Each row carries `__count`. | the list's; `from`? / `to`? where the reading states `over` | each of a log's `register.readings` by its place among them, counted from 0, named by the log's entity; a reading stating `alias` is called `<entity>_<alias>` |
42
- | `<entity>_block_reading_<n>` | <label>: how many <child> rows there are. Each row carries `__count`. | `<entity>_id`; `same_<field>` where it is a list stating `same` | each reading, list or funnel block of the record a row of a book or of a log opens, by its place among that record's blocks, counted from 0, named by the book's or the log's entity |
43
- | `<reading>_bound` | <label> / <bound>: the sum of "<bound>" over the <entity> rows. Each row carries `__count`, `__sum`. | its reading's | a metric whose value `records.limits` bounds: the bound's sum, beside the reading's own |
44
- | `<entity>_export_rows` | Every row of <entity> the reader may see, with every field "<template>" may print. | the list's | a `register.export` that fills a template |
45
- | `<entity>_export_rows_of_<book>` | Every row of <entity> the reader may see, with every field "<template>" may print. | the list's | a report of some of an app's `books`: their rows alone, each book named in turn |
43
+ | `<child>_by_<link>_picks` | The <child> rows no <entity> holds through "<link>" yet, that one <entity> may pick, narrowed by what the reader typed. | `search`?; `same_<link>` per `same` link | a List with `pick`: it picks existing rows into the record |
44
+ | `<reading key>` | <label>: how many <entity> rows there are. Each row carries `__count`. | the list's; `from`? / `to`? where the reading states `over`, or its `tab` is dated `in` a period; `to` where its `tab` is of rows `open` on a day | each Reading of the register List, called by its `key` (`register_reading_<n>`, n counted from 0, only for an app stored before trees) — but the work against its plan (Progress), which reads the list's own rows; none over Books |
45
+ | `<reading key>` | <label>: how many <entity> rows there are. Each row carries `__count`. | `from`? / `to`? where the reading states `over` | each Reading of a dashboard, called by its `key` (`dashboard_reading_<n>` only for an app stored before trees) |
46
+ | `<reading key>` | <label>: how many <child> rows there are. Each row carries `__count`. | `<entity>_id`; `same_<field>` where it is a list stating `same` | each Reading of the record and each List or funnel on it, called by its `key` (`block_reading_<n>` only for an app stored before trees) |
47
+ | `<entity>_<key>` | <label>: how many <entity> rows there are. Each row carries `__count`. | the list's; `from`? / `to`? where the reading states `over` | each Reading of a log's List, called `<entity>_<key>` (`<entity>_register_reading_<n>` only for an app stored before trees) |
48
+ | `<entity>_<key>` | <label>: how many <child> rows there are. Each row carries `__count`. | `<entity>_id`; `same_<field>` where it is a list stating `same` | each Reading of the record a row of a Book or of a log opens and each List or funnel on it, called `<entity>_<key>` by the Book's or the log's entity (`<entity>_block_reading_<n>` only for an app stored before trees) |
49
+ | `<reading>_bound` | <label> / <bound>: the sum of "<bound>" over the <entity> rows. Each row carries `__count`, `__sum`. | its reading's | a Reading whose value `records.limits` bounds: the bound's sum, beside the Reading's own |
50
+ | `<reading>_on_hand` | <label> / <on-hand entity>: how many <on-hand entity> rows there are, per "<field>". Each row carries `<field>`, `__count`. | none: every row of its entity its `where` keeps, whatever the place | a Reading with `on_hand` (a shortfall): the on-hand rows' groups, beside the Reading's own |
51
+ | `<entity>_export_rows` | Every row of <entity> the reader may see, with every field "<template>" may print. | the list's | an Export of the register that fills a template |
52
+ | `<entity>_export_rows_of_<book>` | Every row of <entity> the reader may see, with every field "<template>" may print. | the list's | an Export over some of an app's Books: their rows alone, each Book named in turn |
46
53
  | `<entity>_moves` | Each move in <history> of the <entity> rows handed in: the status it went to, when and by whom, earliest first. | `<entity>_ids` | an entity whose status keeps a history (`records.status.history`) |
47
- | `<entity>_plan_load` | How many <entity> rows each <place> holds, over every table the register reads — the rows with no <place> counted apart. | | a `plan` register: its places' counts of present rows |
54
+ | `<entity>_plan_load` | How many <entity> rows each <place> holds, over every table the register reads — the rows with no <place> counted apart. | | a register laid out as a `plan`: its places' counts of present rows |
48
55
 
49
56
  <!-- generated:end queries -->
50
57
 
@@ -53,10 +60,8 @@ splits by, or per day. Each row carries the alias of each field it splits by, `_
53
60
  day, `__count`, and `__sum` where it sums a number; its description states which. An average is `__sum`
54
61
  over `__count`, both over the rows holding a value.
55
62
 
56
- A reading that states no `alias` is called by its place, so moving it in the model changes the name it is
57
- called by. A place that generates no query keeps its number, so the names may skip one. A reading an agent
58
- or a published API calls states its `alias` (`lotics docs model/readings`). A log's reading is called by the
59
- log's entity before either name: `<entity>_register_reading_<n>`, or `<entity>_<alias>`.
63
+ A Reading is called by its `key` (`model/reading-node`), so moving it in the tree changes no name. A log's Reading is called by the
64
+ log's table before the name: `<entity>_<key>`.
60
65
 
61
66
  A register reading's query reads the rows it names, whatever a screen has open: the rows of its `tab` where
62
67
  it states one, every tab's where it states none — its description says which. One placed in time (`over`)
@@ -64,19 +69,19 @@ reads every day unless the call states `from` or `to`.
64
69
 
65
70
  ### Queries the screen runs for itself
66
71
 
67
- They run like any other, and `get_app_capabilities` leaves them out: each feeds one control, and takes ids
72
+ They run like any other, and `get_app` leaves them out: each feeds one control, and takes ids
68
73
  the screen already holds.
69
74
 
70
75
  <!-- generated:start screen-queries -->
71
76
 
72
77
  | Called | What it says of itself | Takes | Generated for |
73
78
  |---|---|---|---|
74
- | `<child>_<link>_target` | One <entity> row by its id, where a <child> row's "<link>" may point at it. | `<entity>_id` | a picking block whose link a write rule narrows |
79
+ | `<child>_<link>_target` | One <entity> row by its id, where a <child> row's "<link>" may point at it. | `<entity>_id` | a List with `pick` whose link a write rule narrows |
75
80
  | `<entity>_<field>_values` | Each "<field>" the <entity> rows hold, once, most held first, narrowed by what the reader typed. | `search`? | a text field a write rule offers values to (`suggest`) |
76
81
  | `<entity>_pictures` | The picture and name of each <entity> row among the ids handed in. | `<entity>_ids` | a link to several pictured rows |
77
82
  | `<entity>_count_by_<scope>` | How many <entity> rows each <scope entity> holds. | | an app stating `scope` |
78
83
  | `<task>_progress_by_<link>` | How many <task> rows each <entity> row handed in holds, at each status. | `<entity>_ids` | rows whose task rows are counted done |
79
- | `<rows>_measured_by_<link>` | The share of each <entity> row handed in its <rows> rows add, day by day. | `<entity>_ids` | a `progress` reading stating `measured` |
84
+ | `<rows>_measured_by_<link>` | The share of each <entity> row handed in its <rows> rows add, day by day. | `<entity>_ids` | a Progress node stating `measured` |
80
85
  | `<lane>_lanes` | The <lane> rows of one <scope entity>, each a lane of the board. | `<scope entity>_id` | a lanes board inside an app stating `scope` |
81
86
  | `<entity>_same_<link>` | The <entity> rows naming one "<link>". | `same`; `before` where its line reads rows before a moment | a statement line that carries rows (`parts`) |
82
87
 
@@ -91,32 +96,29 @@ the screen already holds.
91
96
  | `create_<entity>` | Opens one row of <entity>. | every field the add asks, and the link it files the row under | an entity the app adds rows of |
92
97
  | `update_<entity>` | Changes one row of <entity>; every input but the row is optional, and one left out is left as it is. | `record_id`, and each field a person edits — a files field or a link to several rows as `<field>_added` / `<field>_removed` | an entity the app edits |
93
98
  | `remove_<entity>` | Removes one row of <entity>. | `record_id` | an entity the app removes rows of |
94
- | `act_<act>` | <label> — on one row of <entity>, refused where its conditions do not hold. | `record_id` — `record_ids` on an act over several rows — and what the act asks | each of `acts` that names no workflow of its own |
95
- | `archive_<act>` | Hands back every file of <label> on one row of <entity> as one archive. | `record_id` | an act making a set of papers (`templates`) |
96
- | `record_activity` | Lists one record's activity, newest first: each save with its fields before and after and who made it, and whether the caller may put it back. `next` is the `after` of the next page. | `record_id`; `after`? | every register app, unless `record.activity` is `false` |
97
- | `record_activity_undo` | Puts back one change entry of a record's activity, as the caller's own save, each field where the record still holds what the entry wrote. The fields kept are handed back with why. | `record_id`; `entry_id` | every register app, unless `record.activity` is `false`, where the app edits that entity |
98
- | `<workflow>_<book>` | The workflow of that name as one of the app's `books`, or a log of it, runs it, where its own table or page makes it differ. | that workflow's | an app stating `books` or `logs` |
99
+ | `act_<act>` | <label> — on one row of <entity>, refused where its conditions do not hold. | `record_id` — `record_ids` on an act over several rows — and what the act asks | each Act that names no workflow of its own |
100
+ | `archive_<act>` | Hands back every file of <label> on one row of <entity> as one archive. | `record_id` | an Act making a set of papers (`templates`) |
101
+ | `<workflow>_<book>` | The workflow of that name as a Book of the app, or a log of it, runs it, where its own table or page makes it differ. | that workflow's | an app with Books or logs |
99
102
 
100
103
  <!-- generated:end workflows -->
101
104
 
102
- A workflow the model declares (`workflows`) and an act that names its own are called by the alias the model
103
- gives them.
105
+ A Workflow node is called by its `key`; an Act that names its own `workflow` is called by that workflow's alias, and every other Act by `act_<key>`.
104
106
 
105
107
  ## Calling a query
106
108
 
107
109
  ```
108
- lotics run get_app_capabilities '{"app_id":"app_…"}'
109
- lotics run run_app_query '{"app_id":"app_…","alias":"hire_list","params":{"status_is":"opt_…","due_back_to":"2031-03-14"}}'
110
+ get_app {"app_id":"app_…"}
111
+ run_app_query {"app_id":"app_…","alias":"order_list","params":{"status_is":"open","due_on_to":"2031-03-14"}}
110
112
  ```
111
113
 
112
114
  | Argument | Does |
113
115
  |---|---|
114
- | `params` | The query's declared params. A required one is always sent; an optional one left out narrows nothing. A select takes an option's value, a link a record id, a member a member id, a date `yyyy-mm-dd` |
115
- | `filters` | Narrows the result further, by the query's output columns (`lotics docs filters`) |
116
+ | `params` | The query's declared params. A required one is always sent; an optional one left out narrows nothing. A select takes the option's alias as `get_app` lists it in `select(label=alias|…)` — a link a record id, a member a member id, a date `yyyy-mm-dd` |
117
+ | `filters` | Narrows the result further, by the query's output columns (`filters`) |
116
118
  | `fields` | The output columns to return, in place of all of them; each row then holds them and the id of the record it is |
117
119
  | `sort` | An order in place of the query's own: `[{ "field_key": "<column>", "order": "asc" }]`, `asc` or `desc`, each `field_key` an output column's name |
118
120
  | `count` | `true`: `total`, the number of rows, in place of the rows; with `count_by`, `counts` per value of one column, keyed by an option's label, a member's name or a linked row's display; with `count_where` — a map of name → filter, never beside `count_by` — `counts` per name. Never with `fields`, `sort`, `limit` or `cursor` |
119
- | `aggregate` | Groups of the result in place of its rows, one row per group: `by` (up to 3 columns), or `day` (a date column, answered as `__day`), and `sum` (a number column, answered as `__sum`), each group with its `__count`. Sent with `params` and `filters` alone |
121
+ | `aggregate` | Groups of the result in place of its rows, one row per group: `by` (up to 3 columns), `bands` (a number column of `by` grouped by its breakpoints), or `day` (a date column, answered as `__day`), and `sum` (a number column, answered as `__sum`), each group with its `__count`. Sent with `params` and `filters` alone. Buckets are by day only: a period's total is one call per period, with `from` and `to` or a date filter |
120
122
  | `limit`, `cursor` | A page — 200 rows at most, 50 where no `limit` is stated — and the `next_cursor` of the page before it; `truncated` says more rows matched than `rows` holds |
121
123
  | `runs` | Several reads in one call, 20 at most, each with its own `alias` and arguments, answered in order under `runs`. Never beside an `alias` of the call's own |
122
124
 
@@ -131,7 +133,7 @@ take are the ones on the query's `returns` line.
131
133
  A list's params are how a person narrows that register: `search`, the row's id on an app that works inside
132
134
  one row (`scope`), and a param for each filter the register states that one value or a range narrows. A
133
135
  filter with no param — on a looked-up link or member, on a number read in each row's own unit or currency,
134
- on a date holding a span, or on a register of several tables (`books`) — is narrowed by `filters`, where a
136
+ on a date holding a span, or on a register of several tables (Books) — is narrowed by `filters`, where a
135
137
  number condition names the unit its value is in as `unit_option`, an option of the figure's select by label
136
138
  or key. Each tab of a register is a query of its own, taking the list's params and the period its rows are
137
139
  read over.
@@ -139,50 +141,54 @@ read over.
139
141
  ## Calling a workflow
140
142
 
141
143
  ```
142
- lotics run run_app_workflow '{"app_id":"app_…","alias":"act_check_out","inputs":{"record_id":"rec_…"},"preview":true}'
143
- lotics run run_app_workflow '{"app_id":"app_…","alias":"act_check_out","inputs":{"record_id":"rec_…"}}'
144
+ run_app_workflow {"app_id":"app_…","alias":"act_hand_over","inputs":{"record_id":"rec_…"},"dry_run":true}
145
+ run_app_workflow {"app_id":"app_…","alias":"act_hand_over","inputs":{"record_id":"rec_…"}}
144
146
  ```
145
147
 
146
148
  | Argument | Does |
147
149
  |---|---|
148
150
  | `inputs` | The workflow's declared inputs. An update takes the record and only the fields that change |
149
- | `preview` | `true`: nothing is written, and the answer is what the run would return, with the calls it would make — or `status: "not_previewed"` where the workflow cannot be previewed (§ A preview) |
150
- | `runs` | Several runs in one call, 50 at most, in order, each its own write, answered under `runs`. The first one refused, failed or not previewed ends the call. An input `{"from_run": 0, "path": "data.record_id"}` is an earlier run's answer at that path. A run states its own `preview`, or takes the call's |
151
+ | `dry_run` | `true`: the run is made and taken back, so nothing is kept, and the answer is what the run would return, with the calls it would make — or `status: "not_previewed"` where the workflow cannot be dry-run (§ A dry run) |
152
+ | `as_member` | With `dry_run`, for a workspace admin: the run is judged as that member would press it — their stage owners, row rules and app access — and writes nothing. Anyone else is refused; `dry_run` on a record tool takes it the same way, and so do `query_records`, `aggregate_records` and `get_record`, which answer what that member would read |
153
+ | `runs` | Several runs in one call, 50 at most, in order, each its own write, answered under `runs`. The first one refused, failed or not dry-run ends the call. An input `{"from_run": 0, "path": "data.record_id"}` is an earlier run's answer at that path. A run states its own `dry_run`, or takes the call's. Dry runs one after another share their rows, taken back together, so a run reads what the one before it made; a run reads a dry run's answer only from a run dry-run in that stretch |
151
154
 
152
155
  ```
153
- lotics run run_app_workflow '{"app_id":"app_…","runs":[
154
- {"alias":"create_hire","inputs":{"customer":"rec_…","machine":"rec_…","due_back":"2031-03-14"}},
155
- {"alias":"act_check_out","inputs":{"record_id":{"from_run":0,"path":"data.record_id"}}}
156
- ]}'
156
+ run_app_workflow {"app_id":"app_…","runs":[
157
+ {"alias":"create_order","inputs":{"customer":"rec_…","item":"rec_…","due_on":"2031-03-14"}},
158
+ {"alias":"act_hand_over","inputs":{"record_id":{"from_run":0,"path":"data.record_id"}}}
159
+ ]}
157
160
  ```
158
161
 
159
- ## A preview
162
+ ## A dry run
160
163
 
161
- A previewed run makes every read the run would make and none of its writes. Its answer carries `status`,
162
- `message`, `field_errors` and `data` as the run would return them, and under `preview`:
164
+ A dry run is the run itself on the real inputs, taken back: it is made in a transaction that is rolled back, so a read after a write reads that write, a branch or a loop walks the values the run would hold, and a call fed by an earlier call's answer gets it. No row, number, event, audit entry or table workflow outlives it. A write of the workspace's rows is made by its table — checks, write rules, stage owners — and the first refusal is the dry run's answer. A connected service's read-only call is made; a service write, a send, a notification or a button that runs another workflow is not, and is answered as a "would" with its payload. A guard (`when`, `requires`, a check, a lock) refuses it with the press's own sentence. A paper it makes is rendered for the answer and its file is removed with the dry run; an act that makes a paper is dry-run like any other, never refused for it. A body that sets the record's status also answers `moves`. Its answer carries `status`,
165
+ `message`, `field_errors` and `data` as the run would return them, and under `dry_run`:
163
166
 
164
167
  | Key | Holds |
165
168
  |---|---|
166
169
  | `available` | `true` |
167
170
  | `writes` | The calls the run would make that are not reads, in order. The app's owner and an admin, who may read the workflow's body, read each call's `step_id`, `tool_name` and `input`. Another member reads each step that would write once: its `step_id` and `tool_name`, and where the step states them outright, the `table_id` it names and how many `records` it lists |
168
- | `unpredicted` | The parts of the answer left out — `message`, `field_errors`, `data` — because each reads what a call the preview did not make would answer |
169
-
170
- A preview says nothing of a call its target refuses when it is made — a locked record, a required field, a
171
- permission — nor of what a write sets off in turn: a table's own workflows, its computed fields.
172
-
173
- A workflow is not previewed where its body reads the answer of a call the preview does not make, to branch,
174
- loop, check or feed another call, where it reads a linked record after such a call, where it runs an agent,
175
- where it waits, where a step cannot be evaluated or sends its call an input the call refuses, and where the
176
- run is past what one preview walks — its steps, the passes of a loop, its reads or the time it takes. The
171
+ | `reads` | The reads made for real, in order, each with its `input` and what it `returned` (cut at 2,000 characters, `truncated`), for the app's owner and an admin |
172
+ | `papers` | Each paper rendered: its `step_id` and `filename`. The file is not kept |
173
+ | `unpredicted` | The parts of the answer left out — `message`, `field_errors`, `data` — because each reads what a call the dry run did not make would answer |
174
+
175
+ What a write sets off once it commits — a table's own workflows, its events — does not happen in a dry run.
176
+
177
+ A workflow is not dry-run where its body reads the answer of a call that leaves the workspace, to branch,
178
+ loop, check or feed another call, where it reads or writes rows after such a call (the run would find them as
179
+ that call left them), where it runs an agent, where it waits, where a step cannot be evaluated, fails
180
+ for a reason that is no table's refusal, or sends its call an input the call refuses, and where the run is past
181
+ what one dry run walks — its steps, the passes of a loop, 50 reads, 50 writes or 20 seconds, which the runs of
182
+ one call share and the database holds it to: a lock wait that outlasts them ends the dry run. The
177
183
  answer is then `status: "not_previewed"` with
178
- `preview: { "available": false, "reason" }`, and nothing ran — within `runs`, it ends the call. The same
184
+ `dry_run: { "available": false, "reason" }`, and nothing ran — within `runs`, it ends the call. The same
179
185
  answers a caller outside the app's organization.
180
186
 
181
187
  ## A reviewed act
182
188
 
183
189
  An act that states `review` runs a workflow that only proposes: call it with the inputs the act asks and it answers,
184
190
  writing nothing, the changeset its body proposes in `data.review` — `table_id`, `creates` (each with its `key`) and
185
- `updates` (each with its `record_id`), each row's `data` holding only the fields that change, by field key, with an
191
+ `updates` (each with its `record_id`), each row's `data` holding only the fields that change, by field alias, with an
186
192
  optional `note` and `ticked` (`false` where the body leaves it for a person to choose), and `unplaced`, what was read
187
193
  and matched nothing; the platform adds a `sha` to every row. Call it again with the same inputs and `approved`,
188
194
  `{ "updates": [{ "record_id", "sha" }], "creates": [{ "key", "sha" }] }`, listing the rows to write with the `sha` each
@@ -190,24 +196,28 @@ carries in the proposal, copied as it came — never computed — and the platfo
190
196
  proposes them, each only where it proposes the values read:
191
197
 
192
198
  ```
193
- lotics run run_app_workflow '{"app_id":"app_…","alias":"match_statement","inputs":{"record_ids":["rec_…"],"statement":"fil_…"}}'
194
- lotics run run_app_workflow '{"app_id":"app_…","alias":"match_statement","inputs":{"record_ids":["rec_…"],"statement":"fil_…","approved":{"updates":[{"record_id":"rec_…","sha":"9f2c…"}],"creates":[{"key":"line-4","sha":"41d0…"}]}}}'
199
+ run_app_workflow {"app_id":"app_…","alias":"match_statement","inputs":{"record_ids":["rec_…"],"statement":"fil_…"}}
200
+ run_app_workflow {"app_id":"app_…","alias":"match_statement","inputs":{"record_ids":["rec_…"],"statement":"fil_…","approved":{"updates":[{"record_id":"rec_…","sha":"9f2c…"}],"creates":[{"key":"line-4","sha":"41d0…"}]}}}
195
201
  ```
196
202
 
197
- The answer's `data.applied` names the run (`execution_id`), the records `updated`, each `created` record by its key,
203
+ The answer's `data.applied` names the run (`execution_id`), the records `updated`, what each held before (`previous`), each `created` record by its key,
198
204
  and each approved row `left` with its `reason`: `not_proposed` (the second call no longer proposes it), `not_acted`
199
205
  (an update of a record the act was not run on), `changed` (the second call proposes other values than were read — read
200
206
  the proposal again), `empty` (an update changing no field), `missing`, `locked` or `conflict`. Every approved row is
201
207
  checked before any is written, and all are written in one transaction or none; the status history of each row written
202
- that moved is recorded by the same run, so the workflow step `undo_record_changes` with that `execution_id` as its `workflow_execution_id` takes back both. An act over every
208
+ that moved is recorded by the same run. `applied.previous` holds, for each record in `updated`, what the fields the call wrote held before (null for an empty one): `update_records` with it as `set` puts them back, and `delete_records` of a `created` record takes that row away. An act over every
203
209
  row in view takes no `record_ids`. A `file` input takes the id of a file uploaded to the workspace.
204
210
 
205
211
  ## A refusal
206
212
 
207
- A workflow re-checks on the server every rule the screen shows: what an act requires, a bound, a duplicate,
208
- whose step it is, a row the caller may not see. A run that is refused writes nothing and answers
209
- `status: "error"` with the sentence a person would read as `message`, and under `field_errors` the sentence
210
- for each input it was refused over. A value of the wrong type, a missing required input and an unknown
213
+ A workflow re-checks on the server what the screen shows of its own act or save: what an act requires, a
214
+ duplicate, whose step it is, a row the caller may not see. Each write it makes also meets the table's own
215
+ rules (a bound, a span, a stage's owner, its lock), which hold on every path a record is written by. A row its table
216
+ locked is refused in every app's act and save, naming who can change it; an app's own Check only makes a row read-only
217
+ in that app. An unlock in an app's run is judged as the member it runs for, never the app's owner. A run refused
218
+ before it writes writes nothing; one whose later write a table's rule refuses keeps the writes before it.
219
+ Either answers `status: "error"` with the sentence a person would read as `message`, and under `field_errors`
220
+ the sentence for each input it was refused over. A value of the wrong type, a missing required input and an unknown
211
221
  alias are refused before the body runs: the call itself fails with the sentence, and `field_errors` where
212
222
  an input is at fault — within `runs`, a run refused for its inputs answers `status: "error"` in its place,
213
223
  and the runs before it stand. A refused read fails the same way, naming the param or the column; within
@@ -215,58 +225,58 @@ and the runs before it stand. A refused read fails the same way, naming the para
215
225
 
216
226
  ## Declaring more
217
227
 
218
- What a person would ask an agent in the app, and no screen draws, is stated in the model beside the app's
219
- acts. Both are checked by `check_model` and compiled by `apply_model` (`lotics docs model/queries`, `lotics docs model/workflows`).
228
+ What the business asks an agent in the app again and again, and no screen draws, is stated in the app's tree as Query and Workflow nodes, beside its
229
+ Acts: every figure or list whose definition is the business's own (open orders, what a customer owes, low stock) and every recurring operation (close the month, send to the accounting service, chase overdue invoices), each with a `description` written for an agent. A question asked once stays on `aggregate_records`. A Workflow gets an Act only where a person presses it; work only an agent does has no button. Both are checked and compiled when the tree is written (`set_app`; `model/query-node`, `model/workflow-node`).
220
230
 
221
- **A query** — the rows of one entity, or one figure of them, in the model's own aliases:
231
+ **A query** — the rows of one table, or one figure of them, in the app's own aliases:
222
232
 
223
233
  ```jsonc
224
- "queries": {
225
- "hires_overdue": {
226
- "description": "A customer's hires still out past the day they were due back, longest overdue first.",
227
- "params": {
228
- "customer": { "type": "record_link", "entity": "customer", "required": false, "description": "Only this customer's hires." },
229
- "as_of": { "type": "date", "description": "The day a hire is overdue on: due back before it." }
230
- },
231
- "rows": "hire",
232
- "fields": ["customer", "machine", "due_back", "daily_rate"],
233
- "where": { "status": ["out"], "customer": { "param": "customer" }, "due_back": { "to": { "param": "as_of" } } },
234
- "sort": { "field": "due_back" }
235
- }
234
+ {
235
+ "type": "Query",
236
+ "key": "orders_overdue",
237
+ "description": "A customer's orders still open past the day they were due, longest overdue first.",
238
+ "params": {
239
+ "customer": { "type": "record_link", "entity": "customer", "required": false, "description": "Only this customer's orders." },
240
+ "as_of": { "type": "date", "description": "The day an order is overdue on: due before it." }
241
+ },
242
+ "rows": "order",
243
+ "fields": ["customer", "item", "due_on", "price"],
244
+ "where": { "status": ["baking"], "customer": { "param": "customer" }, "due_on": { "to": { "param": "as_of" } } },
245
+ "sort": { "field": "due_on" }
236
246
  }
237
247
  ```
238
248
 
239
- It reads under the entity's read scope and, on the app's own entity, exactly the rows its list reads —
240
- so never the entity of an app that states `books`, whose list reads several tables. A condition reading an
249
+ It reads under the table's read scope and, on the app's own table, exactly the rows its list reads —
250
+ so never the table of an app that states Books, whose list reads several tables. A condition reading an
241
251
  optional param narrows nothing when the call leaves it out; no param compares a number read in each row's
242
252
  own unit or currency, or a date holding a span. `"reading"` in place of `"fields"` returns a count or a sum —
243
- `{ "value": "daily_rate", "by": "machine" }` — as a reading does. A declared query, a declared workflow and
244
- a reading's `alias` take no name the app generates (`lotics docs model/app.generated-name`).
253
+ `{ "value": "price", "by": "item" }` — as a Reading does. A Query, a Workflow and
254
+ a Reading's `key` take no name the app generates (`model/app.generated-name`).
245
255
 
246
- **A workflow** — a write no act of the record makes, typed in the model and its body set on the app:
256
+ **A workflow** — a write no Act of the record makes, typed in the app's tree and its code set on the app:
247
257
 
248
258
  ```jsonc
249
- "workflows": {
250
- "import_bookings": {
251
- "description": "Books every hire a booking sheet lists, reusing a hire the sheet already booked.",
252
- "inputs": {
253
- "sheet": { "type": "file", "description": "The booking sheet, one hire per row." },
254
- "booked_by": { "type": "member", "required": false, "description": "Who the hires are booked by; absent, the caller." }
255
- },
256
- "outputs": { "added": { "type": "number" }, "reused": { "type": "number" } }
257
- }
259
+ {
260
+ "type": "Workflow",
261
+ "key": "import_bookings",
262
+ "description": "Books every order a sheet lists, reusing an order the sheet already booked.",
263
+ "inputs": {
264
+ "sheet": { "type": "file", "description": "The order sheet, one order per row." },
265
+ "booked_by": { "type": "member", "required": false, "description": "Who the orders are taken by; absent, the caller." }
266
+ },
267
+ "outputs": { "added": { "type": "number" }, "reused": { "type": "number" } }
258
268
  }
259
269
  ```
260
270
 
261
- `lotics run set_app_workflow` sets its body (`lotics docs workflows`). A query set on a model's app outside
262
- the model (`set_app_queries`, on the CLI or in chat) is removed by the next apply, whose plan and answer note it; declared under
263
- its alias in `queries`, it is the model's. A caller sees the description and
271
+ `set_app_workflow` sets its code (`workflows`). A query set on an app outside
272
+ the tree (`set_app_queries`, on the CLI or in chat) is removed by the next write, whose answer notes it; declared as a Query
273
+ node, it is the tree's. A caller sees the description and
264
274
  each input's `description` and nothing else, so each says what the value is and what leaving it out means;
265
- `check_model` notes one that says nothing.
275
+ the check notes one that says nothing.
266
276
 
267
277
  **Shape each for a caller that is not looking at a screen.** One job a person hands an agent — a paper read,
268
278
  a message answered, a sheet brought in — is one declared workflow named for the job, never a chain of the
269
- generated per-row workflows. Take a list where the job covers many rows, a record's lines among them. A body
279
+ generated per-row workflows. Take a list where the job covers many rows, a record's lines among them. A workflow's code
270
280
  that writes more than one row makes them in one `save_record` workflow step, all or nothing, and recognises
271
281
  what exists by the identity the input itself carries, declared unique on its table, so the same input sent
272
282
  twice updates instead of adding. Return the ids under `outputs`, so the next run can read them. Gate what
@@ -274,17 +284,14 @@ cannot be undone behind an approval step.
274
284
 
275
285
  ## Prove an app as an agent
276
286
 
277
- Run on a throwaway record.
287
+ Run on one of the owner's own rows, moved only as far as its stage; a record made for the run takes a number and a stage move nobody asked for.
278
288
 
279
- 1. `get_app_capabilities` — every job the app's `description` names has an alias that does it or reads it.
289
+ 1. `get_app` — every job the app's `description` names has an alias that does it or reads it.
280
290
  2. `run_app_query` the list with no param, then with each param a person would narrow by; `count: true`
281
291
  answers how many without the rows.
282
292
  3. `run_app_query` `<entity>_record` with one row's id, and each `<child>_by_<link>` under it.
283
- 4. `run_app_workflow` an act with `preview: true`, then without.
293
+ 4. `run_app_workflow` an act with `dry_run: true`, then without.
284
294
  5. `run_app_workflow` an act its record is not ready for: the answer is `status: "error"` with the sentence
285
295
  the screen would show.
286
- 6. `run_app_workflow` `record_activity` for the record, then `record_activity_undo` with the entry the act
287
- wrote: each field the act changed reads as it did before step 4, but one written since, which `kept`
288
- names. A row the act added under the record and a file it made stay.
289
- 7. `run_app_workflow` the app's job workflow with the input a person would hand it — the paper read, the row:
296
+ 6. `run_app_workflow` the app's job workflow with the input a person would hand it — the paper read, the row:
290
297
  it completes in one call. Call it again with the same input: nothing is added.
@@ -0,0 +1,39 @@
1
+ # App architecture — a worked split
2
+
3
+ The questions that split a business into apps are in `first_build`, § 3. This is one split they produce, settled before any tree is written because the split decides who sees what and where each table's rows are worked.
4
+
5
+ One furniture factory, six people, five apps and a dashboard:
6
+
7
+ | App (the job's sentence) | Person | Register | Inside it |
8
+ |---|---|---|---|
9
+ | Sales desk: takes an order from quote to dispatch | Sales rep | Orders | Lines, the price book, the quote paper (an Act), the customer as a picker |
10
+ | Production board: runs each work order through its stages | Foreman | Work orders | Steps, machines as lanes, the Acts that move a stage |
11
+ | Purchasing: orders material against shortages | Buyer | Purchase orders | Lines, goods received, the supplier as a picker |
12
+ | Warehouse: receives, issues and counts | Storekeeper | Stock items | Receive and Issue Acts, the log of movements |
13
+ | Quality: inspects each finished lot | Inspector | Inspections | Checklist, photos, a pass or fail Act |
14
+ | Owner's review: the week across jobs | Owner | none (Readings) | Orders late, stock short, margin by month |
15
+
16
+ Customers, suppliers, machines and the price book are tables with a `records` entry, never apps: each is picked,
17
+ looked up or opened inside the job that uses it. Each app's `name` and `description` is its job's sentence:
18
+
19
+ ```json
20
+ {
21
+ "type": "App",
22
+ "key": "sales_desk",
23
+ "name": "Sales desk",
24
+ "description": "Takes a customer's order from quote to dispatch.",
25
+ "children": [{ "type": "List", "entity": "order" }]
26
+ }
27
+ ```
28
+
29
+ ```json
30
+ {
31
+ "type": "App",
32
+ "key": "production_board",
33
+ "name": "Production board",
34
+ "description": "Runs each work order through cutting, assembly and finishing.",
35
+ "children": [{ "type": "List", "entity": "work_order" }]
36
+ }
37
+ ```
38
+
39
+ Then `set_access` gives the sales desk to the sales group and the production board to the foreman's (`first_build`, § 3). How each tree is composed is `design`.