@lotics/cli 0.315.18 → 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.
- package/AGENTS.md +37 -24
- package/README.md +42 -37
- package/dist/src/cli.js +27215 -48999
- package/dist/src/client.d.ts +12 -88
- package/dist/src/client.js +10 -74
- package/dist/src/invocation.d.ts +1 -1
- package/docs/agent_surface.md +121 -114
- package/docs/app_architecture.md +39 -0
- package/docs/app_bindings.md +24 -62
- package/docs/building_an_app.md +97 -81
- package/docs/cli_reference.md +18 -19
- package/docs/data_model.md +129 -183
- package/docs/design.md +156 -170
- package/docs/document_templates.md +125 -99
- package/docs/examples.md +1599 -1238
- package/docs/field_values.md +14 -11
- package/docs/filters.md +47 -9
- package/docs/first_build.md +180 -0
- package/docs/getting_started.md +15 -0
- package/docs/knowledge_docs.md +73 -209
- package/docs/migration.md +22 -26
- package/docs/workflow_steps.md +22 -579
- package/docs/workflows.md +407 -208
- package/docs/working_in_a_workspace.md +60 -0
- package/package.json +1 -1
package/docs/agent_surface.md
CHANGED
|
@@ -6,19 +6,23 @@ reach them, over MCP and as `lotics run <tool>`:
|
|
|
6
6
|
|
|
7
7
|
| Tool | Answers |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `
|
|
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
|
|
11
|
+
| `run_app_workflow` | One write — or several in order — or a dry run of it that writes nothing |
|
|
12
12
|
|
|
13
|
-
An app
|
|
14
|
-
asks (§ Declaring more). An app's logs (
|
|
15
|
-
|
|
16
|
-
are by its own — `
|
|
17
|
-
(`
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
35
|
-
|
|
|
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
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
| `<entity>
|
|
42
|
-
| `<entity>
|
|
43
|
-
| `<reading>_bound` | <label> / <bound>: the sum of "<bound>" over the <entity> rows. Each row carries `__count`, `__sum`. | its reading's | a
|
|
44
|
-
| `<
|
|
45
|
-
| `<entity>
|
|
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
|
|
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
|
|
57
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
95
|
-
| `archive_<act>` | Hands back every file of <label> on one row of <entity> as one archive. | `record_id` | an
|
|
96
|
-
|
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
115
|
-
| `filters` | Narrows the result further, by the query's output columns (`
|
|
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 (
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
| `
|
|
150
|
-
| `
|
|
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
|
-
|
|
154
|
-
{"alias":"
|
|
155
|
-
{"alias":"
|
|
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
|
|
162
|
+
## A dry run
|
|
160
163
|
|
|
161
|
-
A
|
|
162
|
-
`message`, `field_errors` and `data` as the run would return them, and under `
|
|
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
|
-
| `
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
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,
|
|
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
|
|
208
|
-
whose step it is, a row the caller may not see.
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
219
|
-
|
|
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
|
|
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
|
-
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
240
|
-
so never the
|
|
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": "
|
|
244
|
-
a
|
|
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
|
|
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
|
-
|
|
250
|
-
"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
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
|
-
`
|
|
262
|
-
the
|
|
263
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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. `
|
|
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 `
|
|
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`
|
|
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`.
|