@lotics/cli 0.316.15 → 0.316.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -1730,7 +1730,7 @@ async function saveSignedLinks(links, dir) {
1730
1730
  }
1731
1731
 
1732
1732
  // src/version.ts
1733
- var VERSION = "0.316.15";
1733
+ var VERSION = "0.316.17";
1734
1734
  var APP_SDK_VERSION = "0.124.7";
1735
1735
 
1736
1736
  // src/timezone.ts
@@ -1747,64 +1747,64 @@ function machineTimezone() {
1747
1747
  import { spawn as spawn2 } from "node:child_process";
1748
1748
 
1749
1749
  // docs/getting_started.md
1750
- var getting_started_default = '# Getting started\n\nA workspace is where one business keeps what it knows and does its daily work. **Records** are what it tracks, one table per kind of thing and one row per entry. **Apps** are the screens a team works in, one per person\'s job, drawn over the tables. **Documents** are the quotes, invoices and slips it sends, each a template filled from a record, or from the many rows of a period or a statement. A paper is the workspace\'s once it is made from a template or uploaded and filed on its record; a file left on the host\'s own disk, the user\'s included, reaches no one in the workspace. A procedure or policy no table holds is a **knowledge doc**; a price list, a customer list or the open orders are rows.\n\nOpen the one page for the job. Pages are read with the `docs` tool (`docs path="design"`; `lotics docs design` on the CLI).\n\n- **Build a workspace from a description:** `docs path="first_build"`. Ask the owner once, only what their papers and your defaults cannot settle, usually their papers and lists first, then a whole bakery built call by call: tables with a check, the paper, the app (which apps, a treatment per kind of row, what the check refuses, who gets it), the owner\'s rows, then pressing it.\n- **Change an app:** `docs path="model/changing-an-app"`. `get_app`, then `set_app` with `nodes` and `base_version_id`. A table is changed with `get_table`, then `set_tables` with its `table_id` (`data_model`).\n- **Enforce a rule on a table:** `docs path="workflows/table-checks"`. A condition no create, update or delete may meet, whoever makes it, and the language it is written in.\n- **Make a document:** `docs path="document_templates"`. A template, its slots and loops, and the act that files it on a record.\n- **Enter data, count, answer a question:** `docs path="working_in_a_workspace"`. The right tool for each job; the value each field takes is `field_values`, choosing rows is `filters`.\n- **Reach a connected service, or do what a button cannot:** `docs path="workflows"`. Triggers, steps and expressions; each service step is in `workflow_steps`.\n- **Share an app:** `docs path="first_build"`, \xA7 3. `set_access`, and which app each person gets.\n\nWhich apps a business needs is `first_build`, \xA7 3 (a worked split: `app_architecture`); how each record is drawn is `design`; every key is in `model`; each screen in a complete workspace is `examples`.\n';
1750
+ var getting_started_default = '# Getting started\n\nA workspace is where one business keeps what it knows and does its daily work. **Records** are what it tracks, one table per kind of thing and one row per entry. **Apps** are the screens a team works in, one per person\'s job, drawn over the tables. **Documents** are the quotes, invoices and slips it sends, each a template filled from a record, or from the many rows of a period or a statement. A paper is the workspace\'s once it is made from a template or uploaded and filed on its record; a file left on the host\'s own disk, the user\'s included, reaches no one in the workspace. A procedure or policy no table holds is a **knowledge doc**; a price list, a customer list or the open orders are rows.\n\nOpen the one page for the job. Pages are read with the `docs` tool (`docs path="design"`; `lotics docs design` on the CLI).\n\n- **Build a workspace from a description:** `docs path="first_build"`. Ask the owner once, only what their papers and your defaults cannot settle, usually their papers and lists first, then a whole bakery built call by call: tables with a check, the paper, the app (which apps, a treatment per kind of row, what the check refuses, who gets it), the owner\'s rows, then pressing it.\n- **Change an app:** `docs path="reference/changing-an-app"`. `get_app`, then `set_app` with `nodes` and `base_version_id`. A table is changed with `get_table`, then `set_tables` with its `table_id` (`data_model`).\n- **Enforce a rule on a table:** `docs path="workflows/table-checks"`. A condition no create, update or delete may meet, whoever makes it, and the language it is written in.\n- **Make a document:** `docs path="document_templates"`. A template, its slots and loops, and the act that files it on a record.\n- **Enter data, count, answer a question:** `docs path="working_in_a_workspace"`. The right tool for each job; the value each field takes is `field_values`, choosing rows is `filters`.\n- **Reach a connected service, or do what a button cannot:** `docs path="workflows"`. Triggers, steps and expressions; each service step is in `workflow_steps`.\n- **Share an app:** `docs path="first_build"`, \xA7 3. `set_access`, and which app each person gets.\n\nWhich apps a business needs is `first_build`, \xA7 3 (a worked split: `app_architecture`); how each record is drawn is `design`; every key is in `model`; each screen in a complete workspace is `examples`.\n';
1751
1751
 
1752
1752
  // docs/first_build.md
1753
- var first_build_default = '# First build \u2014 a whole workspace, call by call\n\nA bakery takes custom-cake orders at the counter. An order is a customer and the cakes on it, one line per cake from the menu; it moves taken, baking, ready, collected, and a slip of the cakes prints when it is ready. Six steps, in the order every workspace is built.\n\nStep 0, before the first write: ask the owner once, in one message, only what you cannot settle yourself. Usually: their papers and lists \u2014 a photo, a spreadsheet or a file is fine; they answer most questions (what they send and receive, customers, items, prices) \u2014 and what no paper tells you that would change the tables or who does which step. The owner answers by hand, so every question costs them time: ask nothing they gave already or a paper settles. Each question is one sentence, no sub-questions or bullets under it; nothing else in that message. Wait for the answer, then build; never build first and ask after. Never ask about stages or design: take a default for everything else and list it in your report so the owner can correct it. Load the owner\'s own rows. `get_workspace` shows what already exists; what no one could guess from the data goes into a knowledge doc with `set_knowledge` (`knowledge_docs`).\n\n## 1. Tables and their checks: `set_tables`\n\nOne call, linked tables together; a call names a table by its alias, a new table in the call by its `ref`, which becomes its alias; a link names the other table so. Three kinds appear: a book lines are chosen from (`menu_item`), the record the work moves (`cake_order`) and its lines (`cake_line`). One table per kind of thing, one column per fact, a link wherever one record points at another. A status is the stage of the work, six options or fewer; a touch ("called") is a log row, a money fact ("paid in full") a formula, a quantity a number with its unit in `notation`. Every table people work states `singular` (an app with an Add over a table lacking it is refused), `title` (who or what a row is; a code only where nothing else names it) and `records` (an app over a table without it is refused).\n\n```json\n{\n "tables": [\n {\n "ref": "customer", "label": "Customers", "singular": "Customer", "title": "name",\n "fields": {"name": {"type": "text", "label": "Name", "required": true}},\n "records": {"party": "person"}\n },\n {\n "ref": "menu_item", "label": "Menu", "singular": "Menu item", "title": "name",\n "fields": {\n "name": {"type": "text", "label": "Cake", "required": true},\n "photo": {"type": "files", "label": "Photo"},\n "price": {"type": "number", "label": "Price", "required": true, "notation": {"style": "currency", "currency": "USD"}}\n },\n "records": {"figure": "price", "image": "photo"}\n },\n {\n "ref": "cake_order", "label": "Cake orders", "singular": "Cake order", "title": "reference",\n "fields": {\n "reference": {"type": "autonumber", "label": "Number", "prefix": "CK-", "padding": 3},\n "customer": {"type": "link", "label": "Customer", "table": "customer", "multi": false, "required": true},\n "due_on": {"type": "date", "label": "Pickup on"},\n "pay_by": {"type": "date", "label": "Pay balance by"},\n "lines": {"type": "link", "label": "Cakes", "table": "cake_line", "multi": true, "pair": "order"},\n "total": {"type": "rollup", "label": "Total", "via": "lines", "op": "sum", "field": "amount"},\n "prepaid": {"type": "number", "label": "Prepaid", "notation": {"style": "currency", "currency": "USD"}},\n "balance": {"type": "formula", "label": "Balance", "expression": "{total} - {prepaid}", "notation": {"style": "currency", "currency": "USD"}},\n "paid": {"type": "formula", "label": "Paid", "expression": "{total} > 0 && {balance} <= 0", "output_type": "boolean"},\n "slip": {"type": "files", "label": "Slip"},\n "stage": {"type": "select", "label": "Stage", "default": ["taken"], "options": [\n {"alias": "taken", "label": "Taken", "color": "slate"}, {"alias": "baking", "label": "Baking", "color": "amber"},\n {"alias": "ready", "label": "Ready", "color": "blue"}, {"alias": "collected", "label": "Collected", "color": "green"}\n ]}\n },\n "records": {"subtitle": ["customer"], "figure": "total", "due": ["pay_by"], "settled": "paid", "status": {"field": "stage", "closed": ["collected"]}},\n "checks": {"pay_by_before_pickup": {"on": ["create", "update"], "field": "pay_by",\n "when": "!isNull(record.pay_by) && !isNull(record.due_on)", "fail_when": "record.pay_by > record.due_on",\n "message": "\\"The balance is due on or before the pickup day\\""}}\n },\n {\n "ref": "cake_line", "label": "Cakes on orders", "singular": "Cake", "title": "item",\n "fields": {\n "item": {"type": "link", "label": "Cake", "table": "menu_item", "multi": false, "required": true},\n "order": {"type": "link", "label": "Order", "table": "cake_order", "multi": false, "required": true, "pair": "lines"},\n "quantity": {"type": "number", "label": "Quantity", "required": true, "min": 1},\n "unit_price": {"type": "number", "label": "Unit price", "default_from": "item.price", "notation": {"style": "currency", "currency": "USD"}},\n "amount": {"type": "formula", "label": "Amount", "expression": "{quantity} * {unit_price}", "notation": {"style": "currency", "currency": "USD"}}\n },\n "unique": [["order", "item"]],\n "records": {"subtitle": ["quantity"], "figure": "amount"}\n }\n ]\n}\n```\n\n`stage` is the table\'s status. `party` (`"person"` or `"organization"`) draws a face; `image` is the files field that pictures a row, never the one an Act files its paper in; `settled` is a yes/no that ends the `due` deadlines, so it needs a `due`; `figure` is a number. `unique` is two fields or more no two rows share (one line per cake on an order). A line copies the menu\'s price with `default_from` when made, so a later price change leaves taken orders alone. `min`, `unique` and the check hold on every write, from any app or tool; a check\'s language is `workflows/table-checks`, an effect after any write a table workflow (`workflows/table-workflows`). Every key is in `model/tables`.\n\n## 2. The paper: `set_template`\n\nThe app\'s Act names the template by its alias, made from its name (`order_slip`): the paper comes first.\n\n```\nset_template {"type": "html", "name": "Order slip",\n "template": "<h1>Order {{reference}}</h1><p>{{customer}} \u2014 pickup {{date due_on}}</p>{{#each lines}}<p>{{item}} \xD7 {{quantity}} \u2014 {{money amount \\"USD\\"}}</p>{{/each}}<p>Total {{money total \\"USD\\"}}</p>",\n "variables": {"reference": "string", "customer": "string", "due_on": "string", "lines": "array", "total": "string"}}\n```\n\nEvery paper the owner sends is made, one template each. A slot is named after a field of the record; the order\'s cakes loop under the name of its link to them (`lines`). Fields, loops, layout and papers over many rows: `document_templates`.\n\n## 3. The app and its acts: `set_app`\n\n**Which apps.** One per role\'s work, shaped to the requirement: the list that role opens, and everything about each row on its page whatever its rhythm. One HR person\'s app holds the people and, on each, their contracts, payslips and leave; a business where payroll and hiring are different people\'s work may want an app each. A related row\'s value on the list (the newest contract\'s end)? A lookup column. Work over many rows (a month\'s payroll)? An Act on the list\'s rows. Used only inside another job (the rates a quote is priced from)? A picker on its lines, price copied by `default_from`. Never an app per table.\n\n**The view per kind of row**, chosen before writing the List (a plain register needs none):\n\n| The rows are | The treatment | Keys |\n|---|---|---|\n| People or organisations | `records.party`, photo or logo as `image` | `model/records` |\n| Photos people look at | `records.image`; List `layout: "cards"` | `model/cards` |\n| Held on a day people schedule by | List `layout: "calendar"` | `model/calendar` |\n| Booked on a resource over time | List `layout: {"lanes": {"by": \u2026}}`, table `no_overlap` | `model/lanes` |\n| Standing at places on a floor or yard | List `layout: {"plan": \u2026}` | `model/plan` |\n| People across days | List `layout: "roster"`, the cells\' List inside | `model/roster` |\n| Expected against counted, before against after | a List with `compare: {"before": \u2026, "after": \u2026}` | `model/list-node` |\n| Ranked by a figure | List `sort: {"field": \u2026, "desc": true}` | `model/list-node` |\n| Moved through a status (stages) | an Act per move, `status.dates`, Readings | `model/act-node` |\n| Each belonging to another row | a child List on its Record | `model/list-node` |\n| Counted over a period | Tabs over one `period`; Readings | `model/tab-node` |\n| Papers a case gathers | a List with `expect`; an Act\'s `template` | `model/list-node` |\n\nAny other screen: `design/treatments`.\n\nHere one app, the counter\'s job, naming the tables and the template by the aliases above. The register lists the orders; opening one shows a Section per stage, and each Act sets the next stage. The cakes are a child List of the open order, added in place with `create`. Starting to bake asks for the deposit, readying makes the slip, collecting waits for payment.\n\n```json\n{\n "body": {\n "type": "App", "key": "counter", "name": "Counter", "color": "amber",\n "children": [\n {"type": "List", "entity": "cake_order", "filters": ["customer"], "create": ["customer", "due_on", "pay_by"], "children": [\n {"type": "Record", "door": "page", "children": [\n {"type": "Section", "key": "order", "title": "Order", "at": ["taken", "baking"], "children": [\n "due_on",\n "pay_by",\n {"type": "List", "entity": "cake_line", "create": ["item", "quantity"]},\n {"type": "Act", "key": "start_baking", "label": "Start baking", "when": {"stage": ["taken"]}, "requires": ["due_on"], "asks": ["prepaid"], "set": {"stage": "baking"}},\n {"type": "Act", "key": "ready", "label": "Ready", "when": {"stage": ["baking"]}, "set": {"stage": "ready"}, "template": "order_slip", "into": "slip"}\n ]},\n {"type": "Section", "key": "pickup", "title": "Pickup", "at": ["ready"], "children": [\n {"field": "balance", "edit": false},\n {"field": "slip", "edit": false},\n {"type": "Act", "key": "collect", "label": "Collected", "when": {"stage": ["ready"], "paid": true}, "set": {"stage": "collected"}}\n ]}\n ]}\n ]}\n ]\n }\n}\n```\n\n`set_app` answers with a note for what the rows call for that the tree leaves out; these fold with one: a read-only copy of a placed Field, `create`/`remove: false` on a read-only List, its `app` where one app is over its table, a lone text field\'s `unique`, a patch address unlike the node\'s `key`. **What the check refuses**, each with its id (`model/<id>` is its page):\n\n- `record.placed-once`: a Field, child List, Reading, Statement or Act stands once in a Record, whose head draws the title, subtitle, image, status, figure.\n- `register.filter-unshown`: a `filters` entry (at most 3) is a select, member, link, date, yes/no or number the rows show, or a formula of such; never text, and the status is the chips already.\n- `register.columns-budget`, `block.columns-budget`: `columns` fit the room a desk leaves beside the name, status, deadline and figure.\n- `act.requires-drawn`: an Act\'s `requires` field is drawn in its Section or asked by it.\n- `app.logs`: a further List at the App\'s root is a log (at most 3) with its own `key`, over another table than the register\'s.\n- `act.makes-or-runs`: an Act makes a `template` or runs a `workflow`.\n- `reading.list-app`: a List\'s `app` has its register over that List\'s table.\n- `block.list-opens`: a read-only child List opens its rows in the drawer, so its table is one a List of the app draws, or its `app` names the app that works it.\n- A Field\'s `label` or `when` stands only on a gallery or prose Field (files, markdown text, or `block: true`).\n- `entity.unique`: a `unique` set holds two fields or more of one value each (text, number, date, single select, one-row link).\n- `model.names-declared`: each alias a key names exists (a table, field, option, Act `key`, template).\n- A Query\'s `sort` is `{"field": \u2026, "desc": true}` (`desc` optional) or a list of them, beside `fields`.\n- A workflow writes a link as ids, `[x.id]`; a whole record or bare value is refused.\n\nThen `set_access` gives the app to the people whose job it is: a `principal_type` of `member` (id from `get_workspace`, `kind: "member"`), `member_group` (an admin lists them: `kind: "group"`) or `organization`, a `role` of `user` or `manager`; none loses access.\n\n```\nset_access {"resources": [{"resource_type": "app", "resource_id": "app_\u2026"}], "principals": [{"principal_type": "member", "principal_id": "<member id>", "role": "user"}]}\n```\n\n## 4. What the owner will ask an agent: Query and Workflow nodes\n\nRead the brief again for what the business asks its assistant again and again: a figure or list whose definition is its own (orders waiting for pickup), an operation (chase overdue balances). Declare each as a Query or Workflow node of the app, with a `description` for an agent that has not seen the screen (`agent_surface` \xA7 Declaring more); a connected service (MISA, a carrier) is reached by a Workflow node\'s step (`workflows`); a question asked once stays on `aggregate_records`.\n\n## 5. The owner\'s rows: `create_records`\n\nThe owner\'s own list goes in as rows, in the batches the tool allows: the customers and menu first, then the orders, linked by the ids those calls return, then each order\'s cakes, linked to the order and a menu item. A row created past the first stage (`records.status.stages`) is refused unless that owned stage\'s owner or a workspace admin makes it, so load the owner\'s rows as an admin. Example rows the papers ask for and do not list come from the owner\'s lists and sample, marked as examples, each created at the stage it should show; pressing rows proves the app and never spreads examples across steps; a placeholder ("Demo customer") is never made where the owner\'s own rows exist. The value each field type takes is in `field_values`.\n\n```\ncreate_records {"table_id": "menu_item", "field_keys": ["name", "price"], "rows": [["Lemon cake", 24], ["Chocolate cake", 28]]}\ncreate_records {"table_id": "customer", "field_keys": ["name"], "rows": [["Ana Lopez"]]}\ncreate_records {"table_id": "cake_order", "field_keys": ["customer", "due_on", "pay_by"], "rows": [["<Ana\'s record id>", "2031-03-14", "2031-03-12"]]}\ncreate_records {"table_id": "cake_line", "field_keys": ["order", "item", "quantity"],\n "rows": [["<that order\'s record id>", "<Lemon cake\'s record id>", 2]]}\n```\n\n## 6. Verify: `run_app_workflow`, `screenshot_app`\n\nMove the owner\'s own rows through the app as their person would, in stage order. An Act runs as `act_<key>` (or under the alias of the `workflow` it names), handed what it asks; dry-run first, then press. To prove a rule, send the write with `dry_run: true`; never make a test row or switch a check off. A row ends at the stage the owner says it is at, so press each only that far; where the owner gave no rows, ask for a few.\n\n```\nrun_app_workflow {"app_id": "app_\u2026", "alias": "act_start_baking", "inputs": {"record_id": "rec_\u2026", "prepaid": 20}, "dry_run": true}\nrun_app_workflow {"app_id": "app_\u2026", "alias": "act_start_baking", "inputs": {"record_id": "rec_\u2026", "prepaid": 20}}\nscreenshot_app {"app_id": "app_\u2026"}\n```\n\nFix a failure in the table or app that causes it. Leave standing instructions with `set_workspace` (`instructions`, one entry per rule): who the business is, the words its people use, what any agent working here must keep to. As an admin, invite the people the brief names with `set_member` (`members` keyed by email, role `member` or `admin`); where the tool is missing, say whom to invite. Then show the owner what you built, job by job, each paper they sent and what makes it, with the choices you made for them to change.\n';
1753
+ var first_build_default = '# First build \u2014 a whole workspace, call by call\n\nA bakery takes custom-cake orders at the counter. An order is a customer and the cakes on it, one line per cake from the menu; it moves taken, baking, ready, collected, and a slip of the cakes prints when it is ready. Six steps, in the order every workspace is built.\n\nStep 0, before the first write: ask the owner once, in one message, only what you cannot settle yourself. Usually: their papers and lists \u2014 a photo, a spreadsheet or a file is fine; they answer most questions (what they send and receive, customers, items, prices) \u2014 and what no paper tells you that would change the tables or who does which step. The owner answers by hand, so every question costs them time: ask nothing they gave already or a paper settles. Each question is one sentence, no sub-questions or bullets under it; nothing else in that message. Wait for the answer, then build; never build first and ask after. Never ask about stages or design: take a default for everything else and list it in your report so the owner can correct it. Load the owner\'s own rows. `get_workspace` shows what already exists; what no one could guess from the data goes into a knowledge doc with `set_knowledge` (`knowledge_docs`).\n\n## 1. Tables and their checks: `set_tables`\n\nOne call, linked tables together; a call names a table by its alias, a new table in the call by its `ref`, which becomes its alias; a link names the other table so. Three kinds appear: a book lines are chosen from (`menu_item`), the record the work moves (`cake_order`) and its lines (`cake_line`). One table per kind of thing, one column per fact, a link wherever one record points at another. A status is the stage of the work, six options or fewer; a touch ("called") is a log row, a money fact ("paid in full") a formula, a quantity a number with its unit in `notation`. Every table people work states `singular` (an app with an Add over a table lacking it is refused), `title` (who or what a row is; a code only where nothing else names it) and `records` (an app over a table without it is refused).\n\n```json\n{\n "tables": [\n {\n "ref": "customer", "label": "Customers", "singular": "Customer", "title": "name",\n "fields": {"name": {"type": "text", "label": "Name", "required": true}},\n "records": {"party": "person"}\n },\n {\n "ref": "menu_item", "label": "Menu", "singular": "Menu item", "title": "name",\n "fields": {\n "name": {"type": "text", "label": "Cake", "required": true},\n "photo": {"type": "files", "label": "Photo"},\n "price": {"type": "number", "label": "Price", "required": true, "notation": {"style": "currency", "currency": "USD"}}\n },\n "records": {"figure": "price", "image": "photo"}\n },\n {\n "ref": "cake_order", "label": "Cake orders", "singular": "Cake order", "title": "reference",\n "fields": {\n "reference": {"type": "autonumber", "label": "Number", "prefix": "CK-", "padding": 3},\n "customer": {"type": "link", "label": "Customer", "table": "customer", "multi": false, "required": true},\n "due_on": {"type": "date", "label": "Pickup on"},\n "pay_by": {"type": "date", "label": "Pay balance by"},\n "lines": {"type": "link", "label": "Cakes", "table": "cake_line", "multi": true, "pair": "order"},\n "total": {"type": "rollup", "label": "Total", "via": "lines", "op": "sum", "field": "amount"},\n "prepaid": {"type": "number", "label": "Prepaid", "notation": {"style": "currency", "currency": "USD"}},\n "balance": {"type": "formula", "label": "Balance", "expression": "{total} - {prepaid}", "notation": {"style": "currency", "currency": "USD"}},\n "paid": {"type": "formula", "label": "Paid", "expression": "{total} > 0 && {balance} <= 0", "output_type": "boolean"},\n "slip": {"type": "files", "label": "Slip"},\n "stage": {"type": "select", "label": "Stage", "default": ["taken"], "options": [\n {"alias": "taken", "label": "Taken", "color": "slate"}, {"alias": "baking", "label": "Baking", "color": "amber"},\n {"alias": "ready", "label": "Ready", "color": "blue"}, {"alias": "collected", "label": "Collected", "color": "green"}\n ]}\n },\n "records": {"subtitle": ["customer"], "figure": "total", "due": ["pay_by"], "settled": "paid", "status": {"field": "stage", "closed": ["collected"]}},\n "checks": {"pay_by_before_pickup": {"on": ["create", "update"], "field": "pay_by",\n "when": "!isNull(record.pay_by) && !isNull(record.due_on)", "fail_when": "record.pay_by > record.due_on",\n "message": "\\"The balance is due on or before the pickup day\\""}}\n },\n {\n "ref": "cake_line", "label": "Cakes on orders", "singular": "Cake", "title": "item",\n "fields": {\n "item": {"type": "link", "label": "Cake", "table": "menu_item", "multi": false, "required": true},\n "order": {"type": "link", "label": "Order", "table": "cake_order", "multi": false, "required": true, "pair": "lines"},\n "quantity": {"type": "number", "label": "Quantity", "required": true, "min": 1},\n "unit_price": {"type": "number", "label": "Unit price", "default_from": "item.price", "notation": {"style": "currency", "currency": "USD"}},\n "amount": {"type": "formula", "label": "Amount", "expression": "{quantity} * {unit_price}", "notation": {"style": "currency", "currency": "USD"}}\n },\n "unique": [["order", "item"]],\n "records": {"subtitle": ["quantity"], "figure": "amount"}\n }\n ]\n}\n```\n\n`stage` is the table\'s status. `party` (`"person"` or `"organization"`) draws a face; `image` is the files field that pictures a row, never the one an Act files its paper in; `settled` is a yes/no that ends the `due` deadlines, so it needs a `due`; `figure` is a number. `unique` is two fields or more no two rows share (one line per cake on an order). A line copies the menu\'s price with `default_from` when made, so a later price change leaves taken orders alone. `min`, `unique` and the check hold on every write, from any app or tool; a check\'s language is `workflows/table-checks`, an effect after any write a table workflow (`workflows/table-workflows`). Every key is in `reference/tables`.\n\n## 2. The paper: `set_template`\n\nThe app\'s Act names the template by its alias, made from its name (`order_slip`): the paper comes first.\n\n```\nset_template {"type": "html", "name": "Order slip",\n "template": "<h1>Order {{reference}}</h1><p>{{customer}} \u2014 pickup {{date due_on}}</p>{{#each lines}}<p>{{item}} \xD7 {{quantity}} \u2014 {{money amount \\"USD\\"}}</p>{{/each}}<p>Total {{money total \\"USD\\"}}</p>",\n "variables": {"reference": "string", "customer": "string", "due_on": "string", "lines": "array", "total": "string"}}\n```\n\nEvery paper the owner sends is made, one template each. A slot is named after a field of the record; the order\'s cakes loop under the name of its link to them (`lines`). Fields, loops, layout and papers over many rows: `document_templates`.\n\n## 3. The app and its acts: `set_app`\n\n**Which apps.** One per role\'s work, shaped to the requirement: the list that role opens, and everything about each row on its page whatever its rhythm. One HR person\'s app holds the people and, on each, their contracts, payslips and leave; a business where payroll and hiring are different people\'s work may want an app each. A related row\'s value on the list (the newest contract\'s end)? A lookup column. Work over many rows (a month\'s payroll)? An Act on the list\'s rows. Used only inside another job (the rates a quote is priced from)? A picker on its lines, price copied by `default_from`. Never an app per table.\n\n**The view per kind of row**, chosen before writing the List (a plain register needs none):\n\n| The rows are | The treatment | Keys |\n|---|---|---|\n| People or organisations | `records.party`, photo or logo as `image` | `reference/records` |\n| Photos people look at | `records.image`; List `layout: "cards"` | `reference/cards` |\n| Held on a day people schedule by | List `layout: "calendar"` | `reference/calendar` |\n| Booked on a resource over time | List `layout: {"lanes": {"by": \u2026}}`, table `no_overlap` | `reference/lanes` |\n| Standing at places on a floor or yard | List `layout: {"plan": \u2026}` | `reference/plan` |\n| People across days | List `layout: "roster"`, the cells\' List inside | `reference/roster` |\n| Expected against counted, before against after | a List with `compare: {"before": \u2026, "after": \u2026}` | `reference/list-node` |\n| Ranked by a figure | List `sort: {"field": \u2026, "desc": true}` | `reference/list-node` |\n| Moved through a status (stages) | an Act per move, `status.dates`, Readings | `reference/act-node` |\n| Each belonging to another row | a child List on its Record | `reference/list-node` |\n| Counted over a period | Tabs over one `period`; Readings | `reference/tab-node` |\n| Papers a case gathers | a List with `expect`; an Act\'s `template` | `reference/list-node` |\n\nAny other screen: `design/treatments`.\n\nHere one app, the counter\'s job, naming the tables and the template by the aliases above. The register lists the orders; opening one shows a Section per stage, and each Act sets the next stage. The cakes are a child List of the open order, added in place with `create`. Starting to bake asks for the deposit, readying makes the slip, collecting waits for payment.\n\n```json\n{\n "body": {\n "type": "App", "key": "counter", "name": "Counter", "color": "amber",\n "children": [\n {"type": "List", "entity": "cake_order", "filters": ["customer"], "create": ["customer", "due_on", "pay_by"], "children": [\n {"type": "Record", "door": "page", "children": [\n {"type": "Section", "key": "order", "title": "Order", "at": ["taken", "baking"], "children": [\n "due_on",\n "pay_by",\n {"type": "List", "entity": "cake_line", "create": ["item", "quantity"]},\n {"type": "Act", "key": "start_baking", "label": "Start baking", "when": {"stage": ["taken"]}, "requires": ["due_on"], "asks": ["prepaid"], "set": {"stage": "baking"}},\n {"type": "Act", "key": "ready", "label": "Ready", "when": {"stage": ["baking"]}, "set": {"stage": "ready"}, "template": "order_slip", "into": "slip"}\n ]},\n {"type": "Section", "key": "pickup", "title": "Pickup", "at": ["ready"], "children": [\n {"field": "balance", "edit": false},\n {"field": "slip", "edit": false},\n {"type": "Act", "key": "collect", "label": "Collected", "when": {"stage": ["ready"], "paid": true}, "set": {"stage": "collected"}}\n ]}\n ]}\n ]}\n ]\n }\n}\n```\n\n`set_app` answers with a note for what the rows call for that the tree leaves out; these fold with one: a read-only copy of a placed Field, `create`/`remove: false` on a read-only List, its `app` where one app is over its table, a lone text field\'s `unique`, a patch address unlike the node\'s `key`. **What the check refuses**, each with its id (`reference/<id>` is its page):\n\n- `record.placed-once`: a Field, child List, Reading, Statement or Act stands once in a Record, whose head draws the title, subtitle, image, status, figure.\n- `list.filter-unshown`: a `filters` entry (at most 3) is a select, member, link, date, yes/no or number the rows show, or a formula of such; never text, and the status is the chips already.\n- `list.columns-budget`: `columns` fit the room a desk leaves beside the name, status, deadline and figure.\n- `act.requires-drawn`: an Act\'s `requires` field is drawn in its Section or asked by it.\n- `app.logs`: a further List at the App\'s root is a log (at most 3) with its own `key`, over another table than the register\'s.\n- `act.makes-or-runs`: an Act makes a `template` or runs a `workflow`.\n- `reading.list-app`: a List\'s `app` has its register over that List\'s table.\n- `list.list-opens`: a read-only child List opens its rows in the drawer, so its table is one a List of the app draws, or its `app` names the app that works it.\n- A Field\'s `label` or `when` stands only on a gallery or prose Field (files, markdown text, or `block: true`).\n- `entity.unique`: a `unique` set holds two fields or more of one value each (text, number, date, single select, one-row link).\n- `app.names-declared`: each alias a key names exists (a table, field, option, Act `key`, template).\n- A Query\'s `sort` is `{"field": \u2026, "desc": true}` (`desc` optional) or a list of them, beside `fields`.\n- A workflow writes a link as ids, `[x.id]`; a whole record or bare value is refused.\n\nThen `set_access` gives the app to the people whose job it is: a `principal_type` of `member` (id from `get_workspace`, `kind: "member"`), `member_group` (an admin lists them: `kind: "group"`) or `organization`, a `role` of `user` or `manager`; none loses access.\n\n```\nset_access {"resources": [{"resource_type": "app", "resource_id": "app_\u2026"}], "principals": [{"principal_type": "member", "principal_id": "<member id>", "role": "user"}]}\n```\n\n## 4. What the owner will ask an agent: Query and Workflow nodes\n\nRead the brief again for what the business asks its assistant again and again: a figure or list whose definition is its own (orders waiting for pickup), an operation (chase overdue balances). Declare each as a Query or Workflow node of the app, with a `description` for an agent that has not seen the screen (`agent_surface` \xA7 Declaring more); a connected service (MISA, a carrier) is reached by a Workflow node\'s step (`workflows`); a question asked once stays on `aggregate_records`.\n\n## 5. The owner\'s rows: `create_records`\n\nThe owner\'s own list goes in as rows, in the batches the tool allows: the customers and menu first, then the orders, linked by the ids those calls return, then each order\'s cakes, linked to the order and a menu item. A row created past the first stage (`records.status.stages`) is refused unless that owned stage\'s owner or a workspace admin makes it, so load the owner\'s rows as an admin. Example rows the papers ask for and do not list come from the owner\'s lists and sample, marked as examples, each created at the stage it should show; pressing rows proves the app and never spreads examples across steps; a placeholder ("Demo customer") is never made where the owner\'s own rows exist. The value each field type takes is in `field_values`.\n\n```\ncreate_records {"table_id": "menu_item", "field_keys": ["name", "price"], "rows": [["Lemon cake", 24], ["Chocolate cake", 28]]}\ncreate_records {"table_id": "customer", "field_keys": ["name"], "rows": [["Ana Lopez"]]}\ncreate_records {"table_id": "cake_order", "field_keys": ["customer", "due_on", "pay_by"], "rows": [["<Ana\'s record id>", "2031-03-14", "2031-03-12"]]}\ncreate_records {"table_id": "cake_line", "field_keys": ["order", "item", "quantity"],\n "rows": [["<that order\'s record id>", "<Lemon cake\'s record id>", 2]]}\n```\n\n## 6. Verify: `run_app_workflow`, `screenshot_app`\n\nMove the owner\'s own rows through the app as their person would, in stage order. An Act runs as `act_<key>` (or under the alias of the `workflow` it names), handed what it asks; dry-run first, then press. To prove a rule, send the write with `dry_run: true`; never make a test row or switch a check off. A row ends at the stage the owner says it is at, so press each only that far; where the owner gave no rows, ask for a few.\n\n```\nrun_app_workflow {"app_id": "app_\u2026", "alias": "act_start_baking", "inputs": {"record_id": "rec_\u2026", "prepaid": 20}, "dry_run": true}\nrun_app_workflow {"app_id": "app_\u2026", "alias": "act_start_baking", "inputs": {"record_id": "rec_\u2026", "prepaid": 20}}\nscreenshot_app {"app_id": "app_\u2026"}\n```\n\nFix a failure in the table or app that causes it. Leave standing instructions with `set_workspace` (`instructions`, one entry per rule): who the business is, the words its people use, what any agent working here must keep to. As an admin, invite the people the brief names with `set_member` (`members` keyed by email, role `member` or `admin`); where the tool is missing, say whom to invite. Then show the owner what you built, job by job, each paper they sent and what makes it, with the choices you made for them to change.\n';
1754
1754
 
1755
1755
  // docs/design.md
1756
- var design_default = "# Designing an app\n\nThe treatment each thing a job reads takes. Every prop named here is a prop of a component of the app tree, and the page of the `model` reference stating a treatment's props stands beside it. A treatment worked through in an app of a complete workspace is a page at `examples/<treatment>`. The method, the apps a business needs and the checks a tree meets are in `first_build`.\n\nThe common treatments are the chooser in `first_build`, \xA7 3; every treatment is listed at `design/treatments`.\n\nAn app is a tree of components: a List of one table, the Record a row opens, its Sections, the Acts that move it. The\ntree states COMPOSITION \u2014 which fields, lists and acts each job's screen holds. How each piece looks is the platform's,\none way per concept, and no prop changes it. A component bound to a table draws well with no props, because its defaults\ncome from the table's facts (`records`, the stage, the write rules): a node the tree leaves out draws that default \u2014 a\nList's Readings band, the stage buttons, the picture, a Record's designed Sections, a head kept to its budget. State a\nnode to change its default, or `false` at it to turn it off (`\"readings\": false`, `\"row\": { \"status\": { \"edit\": false } }`,\n`\"row\": { \"image\": false }`). A Field the screens draw is a control, a save, unless it states `edit: false`: an entity drawn read-only gets none, and a stepped status is never a default edit, since Acts move it. A List left as a table is drawn as one, even where a calendar, a face or a picture would\nanswer the reader at a glance, and that is the most common failure of an app: the answer of `set_app` notes what the\nrows call for that no default draws (rows in time, a template's Act, a timeline), and the tree states the ones the job\nneeds.\n\n## Treatments\n\nA treatment named in a section below is one the answer of `set_app` notes when the rows call for it and neither the\ntree nor a platform default draws it, naming the node that would state it. Nothing is written into the tree for the\nauthor: state the ones the job needs. A note refuses nothing.\n\n| The rows are | The treatment | Its props |\n|---|---|---|\n| People or organisations | `records.party`, with a photo or logo as its `image` | `model/records` |\n| Things with a photo, a scan or a paper | `records.image`; List `layout: \"cards\"` where the picture is how a row is found | `model/records`, `model/cards` |\n| Read by their day | List `layout: \"calendar\"`; on a record, a child List with `layout: {\"agenda\": \u2026}` | `model/calendar`, `model/layouts` |\n| Booked on a resource | List `layout: {\"lanes\": {\"by\": \u2026}}`, and the table's `no_overlap` | `model/lanes`, `model/write-rules` |\n| A log of what happened | a child List with `layout: \"timeline\"` | `model/layouts` |\n| Opened to be worked on | the Record's Sections, in the order the work reaches them \u2014 `at` where it moves in steps; fields only one kind of row holds (a delay's cause, a safety issue's hours lost) in a Section whose `when` names that kind | `model/section-node` |\n| Rows that each belong to another row | a child List on that row's Record, with `create` and `remove` \u2014 and a child's own rows a List in that child List's Record, as deep as the data goes; a List with `same` or `app` where read-only, for rows another app works | `model/list-node` |\n| A related row's value on the list (the newest contract's end date) | a `lookup` field of the table, ordered, as a column | `model/lookup` |\n| Moving through a status | an Act per move, `status.dates` for the moment of each stage, and the register's Readings | `model/act-node`, `model/status`, `model/reading-node` |\n| Counted over a period | the register's Tabs over one `period`; a Tab's `totals` for a child's sums before, during and through it; an open Tab's `where` reads only what was true at the period's end, never a status the row moves on from | `model/tab-node` |\n| Papers a case gathers | a List with `expect`; the papers made, an Act's `templates` | `model/list-node`, `model/act-node` |\n| A plan of spans | List `layout: {\"gantt\": {\"start\": \u2026}}` | `model/gantt` |\n| Stood at a place on a plan, flat or in 3D \u2014 pallets, bins | List `layout: {\"plan\": \u2026}`; places state `plan_place` | `model/plan` |\n| Checked item by item against a standard | a List with `checklist: {\"photo\": \u2026, \"clause\": \u2026, \"failed\": [\u2026], \"passed\": \u2026}`: each row a mark (passed at `closed`, failed at a `failed` option, or by the yes/no `passed` names \u2014 a measured verdict, no task or status needed), its photo and the clause it is checked against; with `expect` on the standard's items, each item is a line marked the same way and filled by the block's columns | `model/list-node` |\n| A place before and after the work | a List with `compare: {\"before\": \u2026, \"after\": \u2026}`: two files fields read side by side, or two value fields read as one struck-then-new column | `model/list-node` |\n| Spots on a drawing | `records.pin: {\"on\": \u2026, \"x\": \u2026, \"y\": \u2026}` on the spots' table: a record listing rows that link to them draws the picture with a numbered pin at each; the picture is the `on` row's `records.image` | `model/record-places-pins-and-cadence` |\n| Photos that must be taken on site | `records.on_site: {\"photos\": [\u2026]}`: a photo taken through the camera keeps where the device stood, and a record holding one taken away says so | `model/record-places-pins-and-cadence` |\n| The real size of the thing (a carton, a piece of furniture, a pallet) | `records.size: {\"across\": \u2026, \"along\": \u2026, \"up\": \u2026}`: three number fields in one length unit, read as one fact \"400 \xD7 300 \xD7 250 mm\" after the subtitle; a plan register draws each box at that size | `model/record-places-pins-and-cadence`, `model/plan` |\n| A number painted on the thing, or a barcode or QR code on it | `records.scan: \"iso6346\"` or `\"text\"` (read off one photo by an agent), or `\"code\"` (decoded on the device, no agent): the title read on a phone, beside each search matching it | `model/record-places-pins-and-cadence` |\n| Who did what on which day | List `layout: \"roster\"` with the List of its cells inside | `model/roster` |\n| Someone with no seat signs off one record (a buyer approves a proof, a client signs a handover) | the Record's `Outside`: the Fields and Lists the link shows as they are, and Acts that each move the status out of its `at` \u2014 a link a member makes of the record, never a seat | `model/outside-node` |\n| A record whose page is its own arrangement of pieces the app already states | Record `compose` | `model/composed-page` |\n| Steps people drop out of between | a Reading with `steps` on the record whose child rows count them | `model/reading-node` |\n| A record's own numbers nobody keys, read at a glance | consecutive Readings with no `entity` (two to eight tiles) | `model/reading-node` |\n| Rows that are each a place a record is sent | a List with `expect` on the link to those places and `post`: each ticked, the post written there and read as its feed shows it | `model/publish-desk` |\n| Papers an agent reads, where the owner asked for one | an Act's `intake` with `fills` | `model/act-node` |\n| Calls and visits | an Act's `record` | `model/act-node` |\n| A tool on a record that the model cannot draw (a drawing editor, a folded-box preview) | a custom-code app over the same table, opened by an Act's `open` | `model/act-node` |\n| Changes a workflow proposes that a person approves row by row \u2014 a price list applied, a statement matched to what is owed | an Act's `workflow` with `review`, its body returning a changeset in `data.review` | `model/act-node` |\n| Documents covering lines | a List with `under` | `model/list-node` |\n| Rows on file a record takes in | a List with `pick` | `model/list-node` |\n| One owner's question across jobs | an App of Tabs and Readings and no register | `model/app-node` |\n\n### Faces and pictures\n\n#### Party\n\n`records.party` is `\"person\"` where the rows are people (a contact, a patient, a driver) and\n`\"organization\"` where they are companies (a customer, a supplier, a carrier). Every surface drawing one of\nits rows \u2014 the register's row, a link's chip, a picker's option, the record's header \u2014 then draws a face:\nits `image` (a photo, a logo), else its initials. Without it, a person is drawn as a thing. A workspace\nmember is a `member` field, and is drawn by their face already. Worked: `examples/party`.\n\n#### Image\n\n`records.image` names the files field that pictures a row \u2014 a product's photo, a damage photo, a\nscan, a receipt. Every row of it then leads with its picture, or its paper's preview, wherever it is drawn;\na link to it previews the file. Never a field an Act keeps the paper it makes in (`into`). List\n`layout: \"cards\"` where the picture is how a row is found. A files field the record keeps as a folder of\npapers is drawn as a Field, read whole. Worked: `examples/cards`.\n\n#### A link opens its row\n\nOn a record, a link to one row opens its peek: a drawer on a desk, a sheet on a phone, reading the\nrow's face \u2014 mark, name, status, line and its figure by its field's name \u2014 and nothing to edit. A link read in a\nrow's cell or on its line opens from its name; a link the record keeps as a field rests as its picker, and\nits way out stands at the field's label where the row has somewhere to go. The peek's one act leaves for\nthe app whose register is that table's rows \u2014 the first, in the member's own order of apps, that the member\ncan open \u2014 at the row's address, or goes to the row inside this app where the table is the app's own,\none of its Books' or a log's, in that list \u2014 a log's rows only from their own app. With no such app the peek has no act, and a link answered in what an Act\nasks opens no peek. No prop states it: give the table an app of its own and\nits links lead there. The app opened decides what the member reads of the row; a row it withholds\nlands them on its not-found.\n\n### Rows in time\n\nRows that each stand on a day, or hold a run of days or hours, are read where they stand in time \u2014 never\nas two date columns. A row holds a span when its `records` line (title and subtitle) holds two dates, its\n`records.frees` names the day it ends, or the table's `no_overlap` books it on a row its `by` links to (a\nchair, a machine). A `no_overlap` by a person or a value alone keeps one row at a time each \u2014 a member's periods in\nturn \u2014 read by what they hold.\n\n#### Calendar\n\n`layout: \"calendar\"`: the rows by their day \u2014 a month, a week, a day or a list; an entry at its\nhour where its date holds one, over its span where its line holds a second date. An entry pressed opens a card on\nit: the row, Delete, its Acts, Reschedule (the day alone moves; its hour and its length stay) and Open. Worked:\n`examples/calendar`.\n\n#### Lanes\n\n`layout: {\"lanes\": {\"by\": \u2026}}` with `by` naming a one-row link whose target's rows are the lanes (a chair, a\nmachine, a room), each drawn even when empty: every row a block from the first date on its line to the\nsecond, and a free stretch adds a row there. `loads` reads what a lane carries against what it holds. A\n`no_overlap` `by` the same link refuses two rows on one resource at once. Worked: `examples/lanes`.\n\n#### Gantt\n\n`layout: {\"gantt\": {\"start\": \u2026}}` (and `end`, else the table's first `due`): one bar a row over\nits run of days \u2014 a shipment, a hire, a task of a project \u2014 read against today. `milestones` marks\ndates on the bar, `planned` the plan under it, `progress` how far it is done, `after` the rows it waits on;\nthe List's `group` is its lanes. Worked: `examples/gantt`.\n\n#### Roster\n\n`layout: \"roster\"` with a List inside it over the child whose rows fill the days \u2014 one link back to the\nregister's table and a date on its line: what each row did on each day (shifts, runs, attendance). With\n`expect` (and `of`) on the cells the columns are the options of a select instead of days: which papers each row holds.\nWorked: `examples/roster`.\n\n#### Agenda\n\nA child List with `layout: {\"agenda\": {\u2026}}` on a record: a child's planned entries by day, soonest first, today marked, each with its\npicture; within a day by the hour, or by a single select of the part of the day. `start` counts Day 1 from a\ndate of the record (a trip, a course).\n\n#### Timeline\n\nA child List with `layout: \"timeline\"` on a record: a child read as a log of dated entries, newest first, with a composer \u2014 the\ncalls, visits and notes about the record, each with who and when, and its files where the child's `image`\nnames them. Entries planned ahead are an agenda; the status's moves are its history. Worked:\n`examples/party`.\n\n### Moves and numbers\n\n#### Moves\n\nRows a status stages towards its `closed` options move by Acts: each moves one option on to the next (`when` the\nstatus holds it, `set` to the next), at the foot of the Section whose work it ends. A status with nothing closed is a\ncondition a person sets. A value decided as a row reaches an outcome \u2014 why it was lost, refused, voided \u2014 is that\nAct's `asks`, never a field the reader must remember to fill. An Act reaching past the workspace \u2014 sending an invoice\nto a service, a letter by mail \u2014 runs its `workflow`, and what the workflow keeps from the answer (an invoice's id,\nits link) is named in the Act's `set`. A stage the work only steps aside into from one step\nand leaves only back to it \u2014 a pause, a repair \u2014 is a detour: its option and its Section stand before that step,\nentered by its own Act; listed after, it reads as the work's next step.\n\n#### Stages\n\nName each job's work plan by its shape: a status a person sets; a pipeline, where who holds the row and for how long\nmatter; or tasks, where only how many are done counts (`records.task`). A pipeline states each stage once in\n`records.status.stages` \u2014 its `owner` (a member field, a link's member field, or `{ \"group\": <id> }`, a member group) and its target\n`within` days \u2014 and the server holds it: only the owner's Acts move the row on, and a row created at a stage past the first is a move into that stage from nothing, which the stage's owner, an unowned stage's writer or a workspace admin (loading history) may make, on every path a row is written by, and a level an amount skips stands\nnamed with why. A move forward passes each owned stage between that binds the row, each its owner's. A stage's `when`\nnames the rows it binds, held as an Act's `when` holds them on the row's own fields, as the row stood before a write\nand as the write leaves it; with none, it binds every row. Expense claims over a limit wait for the finance group while\nsmaller ones go straight to payment: `\"finance_review\": { \"owner\": { \"group\": <id> }, \"when\": { \"amount\": { \"gt\": 5000000 } } }`.\nA row at a stage leaves it by its owner whatever its `when`; `dropped` names the closed options\nthat leave the work undone (cancelled, rejected), which whoever holds the row moves it to from any stage. A stage's owner gates who moves a row out of it; who may move a row into a stage (\"only a bookkeeper marks an invoice paid\") is a check. Late rows, rows due soon, a warning the line draws and rows waiting on the reader stand first in a\nregister; a register whose job is a strict order, a first-come queue, states `attention: false`.\n\n#### Stage dates and history\n\nWho changed what and when is the platform's audit trail, never a table and never shown by an app. When a stage's moment matters\nto the business \u2014 reported, filtered, printed \u2014 it is a date on the row that `records.status.dates` stamps as the row\nenters the option, whoever writes: the one way a status is timed, and the moment a stage's `within` runs from (each stage, and\neach open option of a status with stages, naming none gets `<option>_at`). A straight-line process is one select of its steps in order, each dated; a move whose\nday may already be past `asks` that date with `starts: \"today\"`. `records.status.history` names a child the record write path appends one row to per\nstatus move (its link to the row, the new status, the moment and who) for a business that reports on every move: moves\nper person, or time in a stage a row re-enters. An event with facts of its own \u2014 a reason, an amount, a signature \u2014 is\nits own table, made by the act that records it. The rail reads the history. Worked: `examples/approval`, whose history is kept because a group's\nstage is signed once per person, read off who moved the row.\n\n#### Readings\n\nA List's Readings stand above the rows and read the rows under the open tab, never narrowed by the controls below them. Lead with the job's headline number \u2014\nits exception as a formula's yes/no in a Reading's `where` (what is short), never a stage the\nstatus chips already count, nor the overdue rows the register's summary line counts from `records.due` \u2014 then at most the mix (`by` a field) and the movement (`by` a date), varied by job. A movement over a date is stated, never defaulted, and so is a sum: a status register with no single select to split by gets no default band.\nA grid (`by` and `across`) reads on a dashboard. A dashboard \u2014 an App of Readings and no register \u2014 answers one owner's question across jobs, each\nReading windowed by the period the reader picks, and a List hands over the rows to act on. A\ndashboard of many Readings states Tabs (2 to 5) and each Reading the `tab` it stands under, so one sheet draws at a time. A\nRecord's Readings read its own child rows. Sums of each row's child rows are a Tab's `totals`. Worked: `examples/approval`, `examples/dashboard`.\n\n#### Funnel\n\nA Reading with `steps` reads a record's child rows as steps, each a number column summed over them \u2014 a week's channels\nfrom impressions to bookings, an order's lines from ordered to shipped. It states how much of the first step\neach later one keeps, the share lost between two neighbours, and the last over the first. State two to six\n`steps` in one unit; where the path turns \u2014 reach, then conversion \u2014 state two Readings one straight after the\nother, so the second's bars are read over its own first step: they draw on one grid, every bar starting at one\nedge and every value ending at another, each under its own label. A step no row holds a number in reads as not measured: leave it empty, never a zero nobody measured.\n\n#### Priced lines\n\nA quote's items, an estimate's works, a repair sheet's damages are lines priced off a book: a table whose\n`records` names each item, puts the code people type on its line and its price as its `figure`. The line links to\nit, asks the link and its quantities in its List's `create`, and copies the price (`default_from`), so the quote\nkeeps what was quoted when the book moves. A line's amount is a formula, the record's total a rollup over the rows\nthe List shows \u2014 what is not paid for is a second rollup, never a filter on the first, so each group subtotals.\nGroup the lines by the parts the record is quoted in (a container, a room, a leg), a field the line asks. A book worked only while a quote is written is the picker on the quote's lines; a book with a keeper, Acts or a history of its own is\nan app (`first_build` \xA7 3).\n\n### The record\n\n#### Sections\n\nThe Record's Sections, in the order the work reaches them: the fields people write, a Field per paper the record\nkeeps, a child List per child, the Acts at each one's foot. A register's `columns` are in the order the job reads them, as many as fit a desk; what is read only on the way deeper is the Record's, and a figure that is final only from a stage is a Field of that stage's Section, never a column read on every row at every stage. A register laid out in time (calendar, lanes, roster) opens the\ndefault record: every field a person writes. `compose` is not for a record a standard Section order would\ndo, and never for behaviour, which belongs in an Act.\n\n#### Children\n\nRows that each belong to one row of another table (a one-row link to it) are drawn under that row: a List on\nits Record, `layout: \"timeline\"` where they are a log, `layout: {\"agenda\": \u2026}` where they are planned on a day and within it, a roster of\nthem, or lanes of them by that link. Which children a record lists is the author's \u2014 one of them at least. A List\nlisting only some of them narrows by its `where`: a select (one leg's fees), or a yes/no \u2014 a formula's for what is\nstill owed \u2014 on a List that adds no rows. Rows on\nfile before the row they come to belong to \u2014 a job's cost lines before the supplier bill over them \u2014 are picked into\nit: a List with `pick`, often with no `create` and no `remove`.\n\n### Papers\n\n#### Expect\n\nA List with `expect` naming the child's title \u2014 a single select of kinds, or a one-row link to a\ncatalog: one line per kind, the ones not yet filled standing empty, filled in place. `of` narrows the kinds\nto the record's own multi-select of those it needs. The papers a case gathers, the fees a record knows it\nowes. Worked: `examples/case`.\n\n`only` lists the kinds a List holds a line for, so each kind stands in the Section whose job it is, once. The Act making a paper (`paper: \"<child>.<kind>\"`) is its line's button while it applies, the upload in the \u22EF.\n\n#### Templates\n\nA paper the business makes is an Act's `template`, kept in the record's files field `into` \u2014 or several at\nonce, `templates`: listed where the Act stands, each made one as its file, each not yet made a placeholder,\nthe reader ticking which to make. Worked: `examples/case`.\n\n#### Intake\n\nA paper the business is brought and reads is an Act's `intake`: the press takes the papers, an agent reads\nthem, and each is filed as the line of its kind in the child the record `expect`s, while `fills` writes what\nthey state into the record's fields \u2014 a single link as a row the agent finds among those the app reads \u2014 shown before it is saved. Worked: `examples/case`.\n\nAn agent Act \u2014 an `intake`, a `record`'s `fills` \u2014 spends a run on every press, so it is stated only where the\nowner asked for an agent to do that job; otherwise a paper brought is its `expect` line, and a person types what it states.\n\n#### Under\n\nA List with `under`: the documents that cover lines (an invoice over its fee lines) stand as headings\nover the lines they cover, and a line no document covers offers to make one. A document entered as it arrives over lines\nalready on file (a supplier bill) picks them with `pick` instead; choose one per document.\n\n### Calls\n\n#### Record act\n\nAn Act's `record`: the press records a call, visit or meeting \u2014 audio, transcript, screen where captured \u2014 and\nfiles it as a new row of the record's `timeline` child (`into`), stamped `at` and `by`; `fills` lets an agent fill\nthat row's fields from the transcript, where the owner asked. Worked: `examples/calls`.\n\n### Other apps\n\n#### Open act\n\nAn Act's `open`: the id of another app \u2014 usually a custom-code app drawing what the model cannot \u2014 that the press\nopens at this record. It is navigation only: no workflow of either app runs, and what the tool writes it writes\nthrough its own declared workflows. It stands alone with `label` and `when`, on one record of the app's own (an Act\nin a Record's section, or a compose `Act` piece naming it). The target must be an app of the workspace that reaches the\nrecord's table \u2014 a register over it, or custom code declaring no table or declaring it; anything else is refused at\nsave. A reader without `use` on the target meets that app's own \"cannot open\" state.\n\n## The visual bar\n\n- A person shows a face, a thing its picture, a file its preview, a plan its days on a time axis, a\n quantity a meter (`records.limits`) or a chart, a register its Readings under its title, and long text\n reads whole (a markdown Field is a prose block; `block: true` makes a plain long text one).\n- Every app states its tile's colour (the App's `color`), apart from the workspace's other apps where it can be,\n so the launcher tells the desks apart before their names are read.\n- Every category the job reads is coloured: each select option's `color`, and a `mark` where a brand or a\n glyph names it \u2014 on every option of a select read down a column (how it was paid, the channel, the mode).\n- A yes that needs attention (papers the customer still owes) is a `Check` of its List over a yes/no formula, so it\n reads in the warning tone on the row's line; every other yes reads in the success tone.\n- Someone is a face wherever they are read. A person is a member field and a party a row of a table stating\n `party`; where a select's options name people or organisations who are neither, every option's `mark` is\n its face (`{\"kind\": \"avatar\", \"name\": \"person\"}`), never a colour dot.\n- Every files field is drawn somewhere \u2014 a Section's Fields, a Field of the Record, the `image`, or where the\n Act making it stands.\n- The paperwork is the proof of the work, so every paper the job takes in and every document it issues has a\n place on the record, in the Section where that work happens: a files field per purpose (what arrived, what\n was issued to the customer, to the carrier, to the authority), each kept visible after the stage that made it,\n and a source \u2014 a screenshot, an email, a sheet \u2014 on the row whose figure came from it.\n- A screen with none of these is bland, and bland is a finding: text drawn where a treatment above exists\n is a change to the tree.\n\n## Look at it\n\nAfter an apply, the `screenshot_app` tool draws each app as its people see it \u2014 the register, and a record\nby its `path` \u2014 at a desk's width and a phone's, one screen a shot, naming the columns a table left off at\neach width: `screen` names which screen of a long record. From the CLI, `-o <dir>` saves the shots. Beside the shots, `verdict` is what a write of the app's stored tree would refuse it for now, beside its rows, and\nthe notes on it. Judge each shot against \xA7 The visual bar, change the tree with `nodes`, and write again. To go back,\nsend the tree you saved before the change as `body` with `app_id`, nulling an App prop stated since (`words`, `scope`,\n`period`, `icon`); the tables and rows stay as they are.\n";
1756
+ var design_default = "# Designing an app\n\nThe treatment each thing a job reads takes. Every prop named here is a prop of a component of the app tree, and the page of the `model` reference stating a treatment's props stands beside it. A treatment worked through in an app of a complete workspace is a page at `examples/<treatment>`. The method, the apps a business needs and the checks a tree meets are in `first_build`.\n\nThe common treatments are the chooser in `first_build`, \xA7 3; every treatment is listed at `design/treatments`.\n\nAn app is a tree of components: a List of one table, the Record a row opens, its Sections, the Acts that move it. The\ntree states COMPOSITION \u2014 which fields, lists and acts each job's screen holds. How each piece looks is the platform's,\none way per concept, and no prop changes it. A component bound to a table draws well with no props, because its defaults\ncome from the table's facts (`records`, the stage, the write rules): a node the tree leaves out draws that default \u2014 a\nList's Readings band, the stage buttons, the picture, a Record's designed Sections, a head kept to its budget. State a\nnode to change its default, or `false` at it to turn it off (`\"readings\": false`, `\"row\": { \"status\": { \"edit\": false } }`,\n`\"row\": { \"image\": false }`). A Field the screens draw is a control, a save, unless it states `edit: false`: an entity drawn read-only gets none, and a stepped status is never a default edit, since Acts move it. A List left as a table is drawn as one, even where a calendar, a face or a picture would\nanswer the reader at a glance, and that is the most common failure of an app: the answer of `set_app` notes what the\nrows call for that no default draws (rows in time, a template's Act, a timeline), and the tree states the ones the job\nneeds.\n\n## Treatments\n\nA treatment named in a section below is one the answer of `set_app` notes when the rows call for it and neither the\ntree nor a platform default draws it, naming the node that would state it. Nothing is written into the tree for the\nauthor: state the ones the job needs. A note refuses nothing.\n\n| The rows are | The treatment | Its props |\n|---|---|---|\n| People or organisations | `records.party`, with a photo or logo as its `image` | `reference/records` |\n| Things with a photo, a scan or a paper | `records.image`; List `layout: \"cards\"` where the picture is how a row is found | `reference/records`, `reference/cards` |\n| Read by their day | List `layout: \"calendar\"`; on a record, a child List with `layout: {\"agenda\": \u2026}` | `reference/calendar`, `reference/layouts` |\n| Booked on a resource | List `layout: {\"lanes\": {\"by\": \u2026}}`, and the table's `no_overlap` | `reference/lanes`, `reference/write-rules` |\n| A log of what happened | a child List with `layout: \"timeline\"` | `reference/layouts` |\n| Opened to be worked on | the Record's Sections, in the order the work reaches them \u2014 `at` where it moves in steps; fields only one kind of row holds (a delay's cause, a safety issue's hours lost) in a Section whose `when` names that kind | `reference/section-node` |\n| Rows that each belong to another row | a child List on that row's Record, with `create` and `remove` \u2014 and a child's own rows a List in that child List's Record, as deep as the data goes; a List with `same` or `app` where read-only, for rows another app works | `reference/list-node` |\n| A related row's value on the list (the newest contract's end date) | a `lookup` field of the table, ordered, as a column | `reference/lookup` |\n| Moving through a status | an Act per move, `status.dates` for the moment of each stage, and the register's Readings | `reference/act-node`, `reference/status`, `reference/reading-node` |\n| Counted over a period | the register's Tabs over one `period`; a Tab's `totals` for a child's sums before, during and through it; an open Tab's `where` reads only what was true at the period's end, never a status the row moves on from | `reference/tab-node` |\n| Papers a case gathers | a List with `expect`; the papers made, an Act's `templates` | `reference/list-node`, `reference/act-node` |\n| A plan of spans | List `layout: {\"gantt\": {\"start\": \u2026}}` | `reference/gantt` |\n| Stood at a place on a plan, flat or in 3D \u2014 pallets, bins | List `layout: {\"plan\": \u2026}`; places state `plan_place` | `reference/plan` |\n| Checked item by item against a standard | a List with `checklist: {\"photo\": \u2026, \"clause\": \u2026, \"failed\": [\u2026], \"passed\": \u2026}`: each row a mark (passed at `closed`, failed at a `failed` option, or by the yes/no `passed` names \u2014 a measured verdict, no task or status needed), its photo and the clause it is checked against; with `expect` on the standard's items, each item is a line marked the same way and filled by the block's columns | `reference/list-node` |\n| A place before and after the work | a List with `compare: {\"before\": \u2026, \"after\": \u2026}`: two files fields read side by side, or two value fields read as one struck-then-new column | `reference/list-node` |\n| Spots on a drawing | `records.pin: {\"on\": \u2026, \"x\": \u2026, \"y\": \u2026}` on the spots' table: a record listing rows that link to them draws the picture with a numbered pin at each; the picture is the `on` row's `records.image` | `reference/record-places-pins-and-cadence` |\n| Photos that must be taken on site | `records.on_site: {\"photos\": [\u2026]}`: a photo taken through the camera keeps where the device stood, and a record holding one taken away says so | `reference/record-places-pins-and-cadence` |\n| The real size of the thing (a carton, a piece of furniture, a pallet) | `records.size: {\"across\": \u2026, \"along\": \u2026, \"up\": \u2026}`: three number fields in one length unit, read as one fact \"400 \xD7 300 \xD7 250 mm\" after the subtitle; a plan register draws each box at that size | `reference/record-places-pins-and-cadence`, `reference/plan` |\n| A number painted on the thing, or a barcode or QR code on it | `records.scan: \"iso6346\"` or `\"text\"` (read off one photo by an agent), or `\"code\"` (decoded on the device, no agent): the title read on a phone, beside each search matching it | `reference/record-places-pins-and-cadence` |\n| Who did what on which day | List `layout: \"roster\"` with the List of its cells inside | `reference/roster` |\n| Someone with no seat signs off one record (a buyer approves a proof, a client signs a handover) | the Record's `Outside`: the Fields and Lists the link shows as they are, and Acts that each move the status out of its `at` \u2014 a link a member makes of the record, never a seat | `reference/outside-node` |\n| A record whose page is its own arrangement of pieces the app already states | Record `compose` | `reference/composed-page` |\n| Steps people drop out of between | a Reading with `steps` on the record whose child rows count them | `reference/reading-node` |\n| A record's own numbers nobody keys, read at a glance | consecutive Readings with no `entity` (two to eight tiles) | `reference/reading-node` |\n| Rows that are each a place a record is sent | a List with `expect` on the link to those places and `post`: each ticked, the post written there and read as its feed shows it | `reference/publish-desk` |\n| Papers an agent reads, where the owner asked for one | an Act's `intake` with `fills` | `reference/act-node` |\n| Calls and visits | an Act's `record` | `reference/act-node` |\n| A tool on a record that the model cannot draw (a drawing editor, a folded-box preview) | a custom-code app over the same table, opened by an Act's `open` | `reference/act-node` |\n| Changes a workflow proposes that a person approves row by row \u2014 a price list applied, a statement matched to what is owed | an Act's `workflow` with `review`, its body returning a changeset in `data.review` | `reference/act-node` |\n| Documents covering lines | a List with `under` | `reference/list-node` |\n| Rows on file a record takes in | a List with `pick` | `reference/list-node` |\n| One owner's question across jobs | an App of Tabs and Readings and no register | `reference/app-node` |\n\n### Faces and pictures\n\n#### Party\n\n`records.party` is `\"person\"` where the rows are people (a contact, a patient, a driver) and\n`\"organization\"` where they are companies (a customer, a supplier, a carrier). Every surface drawing one of\nits rows \u2014 the register's row, a link's chip, a picker's option, the record's header \u2014 then draws a face:\nits `image` (a photo, a logo), else its initials. Without it, a person is drawn as a thing. A workspace\nmember is a `member` field, and is drawn by their face already. Worked: `examples/party`.\n\n#### Image\n\n`records.image` names the files field that pictures a row \u2014 a product's photo, a damage photo, a\nscan, a receipt. Every row of it then leads with its picture, or its paper's preview, wherever it is drawn;\na link to it previews the file. Never a field an Act keeps the paper it makes in (`into`). List\n`layout: \"cards\"` where the picture is how a row is found. A files field the record keeps as a folder of\npapers is drawn as a Field, read whole. Worked: `examples/cards`.\n\n#### A link opens its row\n\nOn a record, a link to one row opens its peek: a drawer on a desk, a sheet on a phone, reading the\nrow's face \u2014 mark, name, status, line and its figure by its field's name \u2014 and nothing to edit. A link read in a\nrow's cell or on its line opens from its name; a link the record keeps as a field rests as its picker, and\nits way out stands at the field's label where the row has somewhere to go. The peek's one act leaves for\nthe app whose register is that table's rows \u2014 the first, in the member's own order of apps, that the member\ncan open \u2014 at the row's address, or goes to the row inside this app where the table is the app's own,\none of its Books' or a log's, in that list \u2014 a log's rows only from their own app. With no such app the peek has no act, and a link answered in what an Act\nasks opens no peek. No prop states it: give the table an app of its own and\nits links lead there. The app opened decides what the member reads of the row; a row it withholds\nlands them on its not-found.\n\n### Rows in time\n\nRows that each stand on a day, or hold a run of days or hours, are read where they stand in time \u2014 never\nas two date columns. A row holds a span when its `records` line (title and subtitle) holds two dates, its\n`records.frees` names the day it ends, or the table's `no_overlap` books it on a row its `by` links to (a\nchair, a machine). A `no_overlap` by a person or a value alone keeps one row at a time each \u2014 a member's periods in\nturn \u2014 read by what they hold.\n\n#### Calendar\n\n`layout: \"calendar\"`: the rows by their day \u2014 a month, a week, a day or a list; an entry at its\nhour where its date holds one, over its span where its line holds a second date. An entry pressed opens a card on\nit: the row, Delete, its Acts, Reschedule (the day alone moves; its hour and its length stay) and Open. Worked:\n`examples/calendar`.\n\n#### Lanes\n\n`layout: {\"lanes\": {\"by\": \u2026}}` with `by` naming a one-row link whose target's rows are the lanes (a chair, a\nmachine, a room), each drawn even when empty: every row a block from the first date on its line to the\nsecond, and a free stretch adds a row there. `loads` reads what a lane carries against what it holds. A\n`no_overlap` `by` the same link refuses two rows on one resource at once. Worked: `examples/lanes`.\n\n#### Gantt\n\n`layout: {\"gantt\": {\"start\": \u2026}}` (and `end`, else the table's first `due`): one bar a row over\nits run of days \u2014 a shipment, a hire, a task of a project \u2014 read against today. `milestones` marks\ndates on the bar, `planned` the plan under it, `progress` how far it is done, `after` the rows it waits on;\nthe List's `group` is its lanes. Worked: `examples/gantt`.\n\n#### Roster\n\n`layout: \"roster\"` with a List inside it over the child whose rows fill the days \u2014 one link back to the\nregister's table and a date on its line: what each row did on each day (shifts, runs, attendance). With\n`expect` (and `of`) on the cells the columns are the options of a select instead of days: which papers each row holds.\nWorked: `examples/roster`.\n\n#### Agenda\n\nA child List with `layout: {\"agenda\": {\u2026}}` on a record: a child's planned entries by day, soonest first, today marked, each with its\npicture; within a day by the hour, or by a single select of the part of the day. `start` counts Day 1 from a\ndate of the record (a trip, a course).\n\n#### Timeline\n\nA child List with `layout: \"timeline\"` on a record: a child read as a log of dated entries, newest first, with a composer \u2014 the\ncalls, visits and notes about the record, each with who and when, and its files where the child's `image`\nnames them. Entries planned ahead are an agenda; the status's moves are its history. Worked:\n`examples/party`.\n\n### Moves and numbers\n\n#### Moves\n\nRows a status stages towards its `closed` options move by Acts: each moves one option on to the next (`when` the\nstatus holds it, `set` to the next), at the foot of the Section whose work it ends. A status with nothing closed is a\ncondition a person sets. A value decided as a row reaches an outcome \u2014 why it was lost, refused, voided \u2014 is that\nAct's `asks`, never a field the reader must remember to fill. An Act reaching past the workspace \u2014 sending an invoice\nto a service, a letter by mail \u2014 runs its `workflow`, and what the workflow keeps from the answer (an invoice's id,\nits link) is named in the Act's `set`. A stage the work only steps aside into from one step\nand leaves only back to it \u2014 a pause, a repair \u2014 is a detour: its option and its Section stand before that step,\nentered by its own Act; listed after, it reads as the work's next step.\n\n#### Stages\n\nName each job's work plan by its shape: a status a person sets; a pipeline, where who holds the row and for how long\nmatter; or tasks, where only how many are done counts (`records.task`). A pipeline states each stage once in\n`records.status.stages` \u2014 its `owner` (a member field, a link's member field, or `{ \"group\": <id> }`, a member group) and its target\n`within` days \u2014 and the server holds it: only the owner's Acts move the row on, and a row created at a stage past the first is a move into that stage from nothing, which the stage's owner, an unowned stage's writer or a workspace admin (loading history) may make, on every path a row is written by, and a level an amount skips stands\nnamed with why. A move forward passes each owned stage between that binds the row, each its owner's. A stage's `when`\nnames the rows it binds, held as an Act's `when` holds them on the row's own fields, as the row stood before a write\nand as the write leaves it; with none, it binds every row. Expense claims over a limit wait for the finance group while\nsmaller ones go straight to payment: `\"finance_review\": { \"owner\": { \"group\": <id> }, \"when\": { \"amount\": { \"gt\": 5000000 } } }`.\nA row at a stage leaves it by its owner whatever its `when`; `dropped` names the closed options\nthat leave the work undone (cancelled, rejected), which whoever holds the row moves it to from any stage. A stage's owner gates who moves a row out of it; who may move a row into a stage (\"only a bookkeeper marks an invoice paid\") is a check. Late rows, rows due soon, a warning the line draws and rows waiting on the reader stand first in a\nregister; a register whose job is a strict order, a first-come queue, states `attention: false`.\n\n#### Stage dates and history\n\nWho changed what and when is the platform's audit trail, never a table and never shown by an app. When a stage's moment matters\nto the business \u2014 reported, filtered, printed \u2014 it is a date on the row that `records.status.dates` stamps as the row\nenters the option, whoever writes: the one way a status is timed, and the moment a stage's `within` runs from (each stage, and\neach open option of a status with stages, naming none gets `<option>_at`). A straight-line process is one select of its steps in order, each dated; a move whose\nday may already be past `asks` that date with `starts: \"today\"`. `records.status.history` names a child the record write path appends one row to per\nstatus move (its link to the row, the new status, the moment and who) for a business that reports on every move: moves\nper person, or time in a stage a row re-enters. An event with facts of its own \u2014 a reason, an amount, a signature \u2014 is\nits own table, made by the act that records it. The rail reads the history. Worked: `examples/approval`, whose history is kept because a group's\nstage is signed once per person, read off who moved the row.\n\n#### Readings\n\nA List's Readings stand above the rows and read the rows under the open tab, never narrowed by the controls below them. Lead with the job's headline number \u2014\nits exception as a formula's yes/no in a Reading's `where` (what is short), never a stage the\nstatus chips already count, nor the overdue rows the register's summary line counts from `records.due` \u2014 then at most the mix (`by` a field) and the movement (`by` a date), varied by job. A movement over a date is stated, never defaulted, and so is a sum: a status register with no single select to split by gets no default band.\nA grid (`by` and `across`) reads on a dashboard. A dashboard \u2014 an App of Readings and no register \u2014 answers one owner's question across jobs, each\nReading windowed by the period the reader picks, and a List hands over the rows to act on. A\ndashboard of many Readings states Tabs (2 to 5) and each Reading the `tab` it stands under, so one sheet draws at a time. A\nRecord's Readings read its own child rows. Sums of each row's child rows are a Tab's `totals`. Worked: `examples/approval`, `examples/dashboard`.\n\n#### Funnel\n\nA Reading with `steps` reads a record's child rows as steps, each a number column summed over them \u2014 a week's channels\nfrom impressions to bookings, an order's lines from ordered to shipped. It states how much of the first step\neach later one keeps, the share lost between two neighbours, and the last over the first. State two to six\n`steps` in one unit; where the path turns \u2014 reach, then conversion \u2014 state two Readings one straight after the\nother, so the second's bars are read over its own first step: they draw on one grid, every bar starting at one\nedge and every value ending at another, each under its own label. A step no row holds a number in reads as not measured: leave it empty, never a zero nobody measured.\n\n#### Priced lines\n\nA quote's items, an estimate's works, a repair sheet's damages are lines priced off a book: a table whose\n`records` names each item, puts the code people type on its line and its price as its `figure`. The line links to\nit, asks the link and its quantities in its List's `create`, and copies the price (`default_from`), so the quote\nkeeps what was quoted when the book moves. A line's amount is a formula, the record's total a rollup over the rows\nthe List shows \u2014 what is not paid for is a second rollup, never a filter on the first, so each group subtotals.\nGroup the lines by the parts the record is quoted in (a container, a room, a leg), a field the line asks. A book worked only while a quote is written is the picker on the quote's lines; a book with a keeper, Acts or a history of its own is\nan app (`first_build` \xA7 3).\n\n### The record\n\n#### Sections\n\nThe Record's Sections, in the order the work reaches them: the fields people write, a Field per paper the record\nkeeps, a child List per child, the Acts at each one's foot. A register's `columns` are in the order the job reads them, as many as fit a desk; what is read only on the way deeper is the Record's, and a figure that is final only from a stage is a Field of that stage's Section, never a column read on every row at every stage. A register laid out in time (calendar, lanes, roster) opens the\ndefault record: every field a person writes. `compose` is not for a record a standard Section order would\ndo, and never for behaviour, which belongs in an Act.\n\n#### Children\n\nRows that each belong to one row of another table (a one-row link to it) are drawn under that row: a List on\nits Record, `layout: \"timeline\"` where they are a log, `layout: {\"agenda\": \u2026}` where they are planned on a day and within it, a roster of\nthem, or lanes of them by that link. Which children a record lists is the author's \u2014 one of them at least. A List\nlisting only some of them narrows by its `where`: a select (one leg's fees), or a yes/no \u2014 a formula's for what is\nstill owed \u2014 on a List that adds no rows. Rows on\nfile before the row they come to belong to \u2014 a job's cost lines before the supplier bill over them \u2014 are picked into\nit: a List with `pick`, often with no `create` and no `remove`.\n\n### Papers\n\n#### Expect\n\nA List with `expect` naming the child's title \u2014 a single select of kinds, or a one-row link to a\ncatalog: one line per kind, the ones not yet filled standing empty, filled in place. `of` narrows the kinds\nto the record's own multi-select of those it needs. The papers a case gathers, the fees a record knows it\nowes. Worked: `examples/case`.\n\n`only` lists the kinds a List holds a line for, so each kind stands in the Section whose job it is, once. The Act making a paper (`paper: \"<child>.<kind>\"`) is its line's button while it applies, the upload in the \u22EF.\n\n#### Templates\n\nA paper the business makes is an Act's `template`, kept in the record's files field `into` \u2014 or several at\nonce, `templates`: listed where the Act stands, each made one as its file, each not yet made a placeholder,\nthe reader ticking which to make. Worked: `examples/case`.\n\n#### Intake\n\nA paper the business is brought and reads is an Act's `intake`: the press takes the papers, an agent reads\nthem, and each is filed as the line of its kind in the child the record `expect`s, while `fills` writes what\nthey state into the record's fields \u2014 a single link as a row the agent finds among those the app reads \u2014 shown before it is saved. Worked: `examples/case`.\n\nAn agent Act \u2014 an `intake`, a `record`'s `fills` \u2014 spends a run on every press, so it is stated only where the\nowner asked for an agent to do that job; otherwise a paper brought is its `expect` line, and a person types what it states.\n\n#### Under\n\nA List with `under`: the documents that cover lines (an invoice over its fee lines) stand as headings\nover the lines they cover, and a line no document covers offers to make one. A document entered as it arrives over lines\nalready on file (a supplier bill) picks them with `pick` instead; choose one per document.\n\n### Calls\n\n#### Record act\n\nAn Act's `record`: the press records a call, visit or meeting \u2014 audio, transcript, screen where captured \u2014 and\nfiles it as a new row of the record's `timeline` child (`into`), stamped `at` and `by`; `fills` lets an agent fill\nthat row's fields from the transcript, where the owner asked. Worked: `examples/calls`.\n\n### Other apps\n\n#### Open act\n\nAn Act's `open`: the id of another app \u2014 usually a custom-code app drawing what the model cannot \u2014 that the press\nopens at this record. It is navigation only: no workflow of either app runs, and what the tool writes it writes\nthrough its own declared workflows. It stands alone with `label` and `when`, on one record of the app's own (an Act\nin a Record's section, or a compose `Act` piece naming it). The target must be an app of the workspace that reaches the\nrecord's table \u2014 a register over it, or custom code declaring no table or declaring it; anything else is refused at\nsave. A reader without `use` on the target meets that app's own \"cannot open\" state.\n\n## The visual bar\n\n- A person shows a face, a thing its picture, a file its preview, a plan its days on a time axis, a\n quantity a meter (`records.limits`) or a chart, a register its Readings under its title, and long text\n reads whole (a markdown Field is a prose block; `block: true` makes a plain long text one).\n- Every app states its tile's colour (the App's `color`), apart from the workspace's other apps where it can be,\n so the launcher tells the desks apart before their names are read.\n- Every category the job reads is coloured: each select option's `color`, and a `mark` where a brand or a\n glyph names it \u2014 on every option of a select read down a column (how it was paid, the channel, the mode).\n- A yes that needs attention (papers the customer still owes) is a `Check` of its List over a yes/no formula, so it\n reads in the warning tone on the row's line; every other yes reads in the success tone.\n- Someone is a face wherever they are read. A person is a member field and a party a row of a table stating\n `party`; where a select's options name people or organisations who are neither, every option's `mark` is\n its face (`{\"kind\": \"avatar\", \"name\": \"person\"}`), never a colour dot.\n- Every files field is drawn somewhere \u2014 a Section's Fields, a Field of the Record, the `image`, or where the\n Act making it stands.\n- The paperwork is the proof of the work, so every paper the job takes in and every document it issues has a\n place on the record, in the Section where that work happens: a files field per purpose (what arrived, what\n was issued to the customer, to the carrier, to the authority), each kept visible after the stage that made it,\n and a source \u2014 a screenshot, an email, a sheet \u2014 on the row whose figure came from it.\n- A screen with none of these is bland, and bland is a finding: text drawn where a treatment above exists\n is a change to the tree.\n\n## Look at it\n\nAfter an apply, the `screenshot_app` tool draws each app as its people see it \u2014 the register, and a record\nby its `path` \u2014 at a desk's width and a phone's, one screen a shot, naming the columns a table left off at\neach width: `screen` names which screen of a long record. From the CLI, `-o <dir>` saves the shots. Beside the shots, `verdict` is what a write of the app's stored tree would refuse it for now, beside its rows, and\nthe notes on it. Judge each shot against \xA7 The visual bar, change the tree with `nodes`, and write again. To go back,\nsend the tree you saved before the change as `body` with `app_id`, nulling an App prop stated since (`words`, `scope`,\n`period`, `icon`); the tables and rows stay as they are.\n";
1757
1757
 
1758
1758
  // docs/app_architecture.md
1759
1759
  var app_architecture_default = '# App architecture \u2014 a worked split\n\nThe questions that split a business into apps are in `first_build`, \xA7 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.\n\nOne furniture factory, six people, five apps and a dashboard:\n\n| App (the job\'s sentence) | Person | Register | Inside it |\n|---|---|---|---|\n| 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 |\n| Production board: runs each work order through its stages | Foreman | Work orders | Steps, machines as lanes, the Acts that move a stage |\n| Purchasing: orders material against shortages | Buyer | Purchase orders | Lines, goods received, the supplier as a picker |\n| Warehouse: receives, issues and counts | Storekeeper | Stock items | Receive and Issue Acts, the log of movements |\n| Quality: inspects each finished lot | Inspector | Inspections | Checklist, photos, a pass or fail Act |\n| Owner\'s review: the week across jobs | Owner | none (Readings) | Orders late, stock short, margin by month |\n\nCustomers, suppliers, machines and the price book are tables with a `records` entry, never apps: each is picked,\nlooked up or opened inside the job that uses it. Each app\'s `name` and `description` is its job\'s sentence:\n\n```json\n{\n "type": "App",\n "key": "sales_desk",\n "name": "Sales desk",\n "description": "Takes a customer\'s order from quote to dispatch.",\n "children": [{ "type": "List", "entity": "order" }]\n}\n```\n\n```json\n{\n "type": "App",\n "key": "production_board",\n "name": "Production board",\n "description": "Runs each work order through cutting, assembly and finishing.",\n "children": [{ "type": "List", "entity": "work_order" }]\n}\n```\n\nThen `set_access` gives the sales desk to the sales group and the production board to the foreman\'s (`first_build`, \xA7 3). How each tree is composed is `design`.\n';
1760
1760
 
1761
1761
  // docs/working_in_a_workspace.md
1762
- var working_in_a_workspace_default = "# Working in a workspace \u2014 the right means for each job\n\nOnce tables and apps exist, an agent does the team's work through them. The means depends on what the work is.\n\n| The work | The means | Why |\n|---|---|---|\n| A business operation: move an order on, issue a paper, notify someone | `run_app_workflow` on the app's act, `act_<key>` or the `workflow` it names | The outcome equals a person's press: status, stamps, documents, notifications. `update_records` on an act's field skips all of them. |\n| Data entry, a correction, loading the owner's lists | `create_records`, `update_records` | The table's rules hold for every writer. |\n| A rule or policy the business wrote | `get_workspace` with `search`, then `read_knowledge` | The business's own words, not a guess. |\n| A file someone hands over | `read_file_content`; `upload_file` to keep it (over MCP or the CLI) | Reads its content; the upload files it in the workspace. When your host passes no bytes, `upload_file` with `person: true` gives the person a link to drop it on. |\n| A paper an app makes | the act that makes it, which files it on the record | `generate_document` makes an unattached file, for a paper no app makes. |\n| A connected service (KiotViet, MISA, a carrier) | a workflow step, reached through a Workflow node | No tool calls a service directly (`workflows/reaching-a-connected-service`). |\n| A figure or list the business defines, or an operation it repeats | the app's declared Query or Workflow node: `run_app_query`, `run_app_workflow` | The node is the business's own definition, found by `get_workspace` with `search` and read in `get_app`. |\n| A count or sum no app answers | `aggregate_records` (grouped by `group_by`), `query_records` | |\n\nFind the means in this order: `get_workspace` maps the workspace; `search` it by what is asked (\"overdue\"), which matches an app's declared Query and Workflow keys and descriptions and names the matching keys under the app; `get_app` lists that app's queries and workflows with their inputs. Run the declared node. Only a question no node answers goes to `aggregate_records`.\n\n## Workspace settings and standing instructions\n\n`get_workspace` shows the workspace's settings (name, timezone, default currency) and, with kind `instruction`, its standing instructions. `set_workspace` changes them in one all-or-nothing patch: a setting it leaves out is unchanged, and `instructions` is a map keyed by instruction name where an entry creates or merges and `null` removes. A workspace-wide instruction arrives with the connection, in the instructions a host reads when an agent connects; the answer of `set_workspace` names any that no longer fit that block. An administrator writes them.\n\n## People\n\n`get_workspace` with kind `member` lists the members. `set_member` is an admin's: `members` is a map keyed by an email address or a member id, where an email no member holds invites that person (`role` is `member` or `admin`; the invitation email goes out), a current member's entry sets `name`, a removed member's entry restores them, and `null` removes a member or cancels the invitation pending to that address. A member's role cannot be changed through it. The whole map is checked before anything is written or sent. A member's tool list does not carry `set_member`, `set_group` or `set_workspace`.\n\n## Doing an operation\n\n`get_app` lists the app's acts as `act_<key>` (or the `workflow` an Act names) with what each takes. `run_app_workflow` with `dry_run: true` writes nothing\nand answers what the run would do; the same call without it runs. An act refuses a row its conditions do not hold for,\nand says why (`agent_surface` \xA7 A refusal).\n\n## Writing records\n\n`create_records` takes 50 records a call, 20 or more of one shape in columnar form (`field_keys` once, then `rows`);\n`update_records` changes one or many. Both name fields and select options by alias, and `field_values` gives each\ntype's value. Every write meets the table's rules (`model/write-rules`): every broken rule is listed at once, and a\nlocked record answers under `blocked`. The owner's own lists (a menu with prices, customers, open orders) are rows of their\ntables, loaded this way after `set_tables`.\n\n## Answering a question\n\n1. `get_app`: a Reading's query (its `key`) is the business's own figure for it. `run_app_query` with `alias` and\n `params` returns it; `filters`, `fields`, `sort` and `count` narrow the result.\n2. A period's total is one call per period, each with `from` and `to` or a date filter. `aggregate` groups a result by\n a column (`by`), a number's bands (`bands`) or a date column's day (`day`), and sums one (`sum`).\n3. A question no app answers is `aggregate_records` (counts and sums grouped by a field) or `query_records`, over one\n table; `filters` is the grammar of both.\n\n## A worked day\n\nA customer asks for the status of order 1042, then the order is shipped:\n\n```\nget_workspace kind \"app\" \u2192 app_id of the sales desk\nget_app app_id \u2192 aliases: order_list, act_ship, \u2026\nrun_app_query app_id, alias \"order_list\", params {\"search\": \"1042\"}\nrun_app_workflow app_id, alias \"act_ship\", inputs {\"record_id\": \"rec_\u2026\"}, dry_run true\nrun_app_workflow the same call without dry_run\naggregate_records table_id, group_by \"stage\" \u2192 how many orders stand at each stage now\n```\n";
1762
+ var working_in_a_workspace_default = "# Working in a workspace \u2014 the right means for each job\n\nOnce tables and apps exist, an agent does the team's work through them. The means depends on what the work is.\n\n| The work | The means | Why |\n|---|---|---|\n| A business operation: move an order on, issue a paper, notify someone | `run_app_workflow` on the app's act, `act_<key>` or the `workflow` it names | The outcome equals a person's press: status, stamps, documents, notifications. `update_records` on an act's field skips all of them. |\n| Data entry, a correction, loading the owner's lists | `create_records`, `update_records` | The table's rules hold for every writer. |\n| A rule or policy the business wrote | `get_workspace` with `search`, then `read_knowledge` | The business's own words, not a guess. |\n| A file someone hands over | `read_file_content`; `upload_file` to keep it (over MCP or the CLI) | Reads its content; the upload files it in the workspace. When your host passes no bytes, `upload_file` with `person: true` gives the person a link to drop it on. |\n| A paper an app makes | the act that makes it, which files it on the record | `generate_document` makes an unattached file, for a paper no app makes. |\n| A connected service (KiotViet, MISA, a carrier) | a workflow step, reached through a Workflow node | No tool calls a service directly (`workflows/reaching-a-connected-service`). |\n| A figure or list the business defines, or an operation it repeats | the app's declared Query or Workflow node: `run_app_query`, `run_app_workflow` | The node is the business's own definition, found by `get_workspace` with `search` and read in `get_app`. |\n| A count or sum no app answers | `aggregate_records` (grouped by `group_by`), `query_records` | |\n\nFind the means in this order: `get_workspace` maps the workspace; `search` it by what is asked (\"overdue\"), which matches an app's declared Query and Workflow keys and descriptions and names the matching keys under the app; `get_app` lists that app's queries and workflows with their inputs. Run the declared node. Only a question no node answers goes to `aggregate_records`.\n\n## Workspace settings and standing instructions\n\n`get_workspace` shows the workspace's settings (name, timezone, default currency) and, with kind `instruction`, its standing instructions. `set_workspace` changes them in one all-or-nothing patch: a setting it leaves out is unchanged, and `instructions` is a map keyed by instruction name where an entry creates or merges and `null` removes. A workspace-wide instruction arrives with the connection, in the instructions a host reads when an agent connects; the answer of `set_workspace` names any that no longer fit that block. An administrator writes them.\n\n## People\n\n`get_workspace` with kind `member` lists the members. `set_member` is an admin's: `members` is a map keyed by an email address or a member id, where an email no member holds invites that person (`role` is `member` or `admin`; the invitation email goes out), a current member's entry sets `name`, a removed member's entry restores them, and `null` removes a member or cancels the invitation pending to that address. A member's role cannot be changed through it. The whole map is checked before anything is written or sent. A member's tool list does not carry `set_member`, `set_group` or `set_workspace`.\n\n## Doing an operation\n\n`get_app` lists the app's acts as `act_<key>` (or the `workflow` an Act names) with what each takes. `run_app_workflow` with `dry_run: true` writes nothing\nand answers what the run would do; the same call without it runs. An act refuses a row its conditions do not hold for,\nand says why (`agent_surface` \xA7 A refusal).\n\n## Writing records\n\n`create_records` takes 50 records a call, 20 or more of one shape in columnar form (`field_keys` once, then `rows`);\n`update_records` changes one or many. Both name fields and select options by alias, and `field_values` gives each\ntype's value. Every write meets the table's rules (`reference/write-rules`): every broken rule is listed at once, and a\nlocked record answers under `blocked`. The owner's own lists (a menu with prices, customers, open orders) are rows of their\ntables, loaded this way after `set_tables`.\n\n## Answering a question\n\n1. `get_app`: a Reading's query (its `key`) is the business's own figure for it. `run_app_query` with `alias` and\n `params` returns it; `filters`, `fields`, `sort` and `count` narrow the result.\n2. A period's total is one call per period, each with `from` and `to` or a date filter. `aggregate` groups a result by\n a column (`by`), a number's bands (`bands`) or a date column's day (`day`), and sums one (`sum`).\n3. A question no app answers is `aggregate_records` (counts and sums grouped by a field) or `query_records`, over one\n table; `filters` is the grammar of both.\n\n## A worked day\n\nA customer asks for the status of order 1042, then the order is shipped:\n\n```\nget_workspace kind \"app\" \u2192 app_id of the sales desk\nget_app app_id \u2192 aliases: order_list, act_ship, \u2026\nrun_app_query app_id, alias \"order_list\", params {\"search\": \"1042\"}\nrun_app_workflow app_id, alias \"act_ship\", inputs {\"record_id\": \"rec_\u2026\"}, dry_run true\nrun_app_workflow the same call without dry_run\naggregate_records table_id, group_by \"stage\" \u2192 how many orders stand at each stage now\n```\n";
1763
1763
 
1764
- // src/model_reference.md
1765
- var model_reference_default = '# Tables and apps\n\nThe tables of a workspace and the apps over them, each a JSON document. A table is a document, an item of\nthe tool `set_tables`: its fields, options, views, how a row is recognised, and the rules its writes\nfollow. An app is a tree of components, the `body` of the tool `set_app`: one working register over one table, or a\ndashboard of readings. Each \xA7 is its own page, `model/<section>`.\nWhich treatment each job\'s screen needs: the `design` reference. Each treatment worked through in an\napp: the `examples` reference.\n\n**What the documents compose with**\n\n- **Tables and fields** (\xA7 Tables, \xA7 Read scope, \xA7 Field) \u2014 a table\'s columns, options and links. A table document sits\n in the `tables` list of a `set_tables` call; a field sits under its alias in the table\'s `fields`.\n- **Records** (\xA7 Records, then \xA7 Status) \u2014 how a row of each table is RECOGNISED: its title, the line\n under it, its picture, its status, the one number it stands for. Stated once per table\n and read by every surface that draws one of its rows.\n- **Write rules** (\xA7 Write rules) \u2014 what a write finds, bounds and picks among.\n- **Apps** (\xA7 App tree, then a page each: \xA7 Changing an app, \xA7 What an app may write, \xA7 App node, \xA7 List node, \xA7 Layouts, \xA7 Time and place layouts, \xA7 Publish desk,\n \xA7 Record node, \xA7 Section node, \xA7 Side node, \xA7 Page node, \xA7 Outside node, \xA7 Tab node, \xA7 Field node, \xA7 Act node, \xA7 Check node,\n \xA7 Reading node, \xA7 Progress node, \xA7 Statement node, \xA7 Export node, \xA7 Book node, \xA7 Query node, \xA7 Workflow node,\n \xA7 Composed page, \xA7 Tasks, \xA7 App rules, \xA7 Reading rules, \xA7 Statement rules, \xA7 Check rules, \xA7 Register rules, \xA7 Block rules, \xA7 Record rules, \xA7 Act rules) \u2014 one working register over one table and the record each row opens,\n with the logs beside it: which fields go where, the acts, and the checks that guard them \u2014 or a dashboard of readings.\n How each thing LOOKS is the runtime\'s, one treatment per concept; no prop here changes it.\n A screen the job needs and no component states is told to the person \u2014 the job, what the app\n drew, the word wanted \u2014 never props bent to approximate it.\n\n**The working order**\n\n1. The tables and fields the jobs touch, and a `records` entry for every table an\n app lists, opens or picks. Which apps a business needs is `first_build`, \xA7 3.\n2. One `set_tables` call with the tables: a link names the table it points at by the `ref` of another\n item of the call, and the answer carries every table id and the aliases of the fields it added. A\n call carries at most 50 tables, 200 first rows a table, and 2000 rows and 2000 documents in all; more\n tables go in a further call, which names the earlier tables by alias. A call is checked whole and writes\n nothing when refused, listing everything wrong; `dry_run` is for reading the grants a write would make or\n deciding `force`.\n3. One `set_app` call per app. The tree names its tables and document templates by alias wherever it points at one;\n the App node\'s `tables` and `templates` maps are filled by `set_app`, never written.\n `set_app` with the tree as `body` creates the app, named by its `name`; a refused tree writes nothing and is listed whole, and `dry_run` is for reading the permissions the write\n would grant and remove (on the CLI, `lotics app apply <file>`, with `--check` for `dry_run`).\n4. Change an app afterwards one node at a time: `get_app` prints the `tree:`, each node\'s address under `node addresses:`\n and the `version` line, which is the `base_version_id` to send (the tree and addresses go to the app\'s author alone);\n `set_app` with `app_id`, `base_version_id` and `nodes`, a map from a node\'s address to a\n patch, changes it (`model/changing-an-app`), and its answer states the permissions the change grants and removes. A table\n is changed by `set_tables` with its `table_id`, a merge patch over the document `get_table` answers. To go back,\n send the tree you saved before the change as `body` with `app_id`: it replaces `children` but keeps every App prop it\n omits (`words`, `scope`, `period`, `icon`), so null those. (On the CLI, `lotics app pull <app_id>` writes the tree as JSON and `lotics app apply\n <file>` sends an edited file, refused when the app has moved past the version the file was pulled at.) An app\n saved before apps were trees has no tree: `get_app` says so beside what it lets a person write, and `set_app` with\n `app_id` changes it by its whole tree as `body`, which replaces it, or by its name, description, icon and color alone.\n\n## The rules\n\n- **A `set_tables` call is all or nothing.** Every item is checked before any is written; an item\n without `table_id` makes a table, one with it patches that table.\n- **An app\'s labels decide its language.** A body names no locale \u2014 the words an\n app draws itself (its buttons, empty states, refusals) are in the language its\n own labels are written in, so a Vietnamese desk beside English ones answers in\n Vietnamese. The generated screens read the kit\'s\n pack, and a generated WRITE cannot: its refusals run on the server, so they are\n worded in that same language. Mix the two within one app and it answers in two. The app\'s own `words`\n (its words for a table, a field or an option) count as its labels: Vietnamese words over an English table\n make the app answer in Vietnamese.\n- **`set_tables` and `set_app` check all of it before anything is written**, and\n report every problem in one run rather than the first. The tables half of this\n page ends its keys with the rules the check enforces there, and \xA7 App rules and the pages after it hold the app\'s, one sentence each\n under an id; a finding names its rule\'s id (`[register.filter]`), and\n `model/<rule id>` is that one rule\'s page.\n\n<!-- generated:start rules-model -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `model.schema` | Every key is one its table lists, holding the type its row gives, and every required key is stated. |\n| `model.names-declared` | Every alias a key names is declared: a table of the App\'s `tables`, a field of the table the node reads, an option of the select it names, an Act `key` of the App, a template of its `templates`, or an app. |\n| `model.tables` | A call declares at least one table and at most 50; send the rest in a second call. |\n| `model.rows-cap` | Rows are a sample: at most 200 per entity, 2000 per call and 2000 documents attached \u2014 a real data set goes in with `create_records` once the tables exist. |\n| `model.alias-unique` | An alias is unique where it is named: tables, fields and views in their table, options in their select. |\n| `model.label-unique` | A label is unique where it is found by its label: fields and views in their table, options in their select \u2014 and no view takes its table\'s own label, which names the whole-table grid. |\n| `model.template-kind` | An Act\'s `template` is an html, excel or word document template and an Export\'s `template` an html or excel one, never an email or pdf-form one. |\n| `model.template-marker` | Every marker of a Word template an Act makes is filled by that Act: a field of its entity, or a list of the child rows its record lists (the entity\'s own alias over several rows). |\n| `model.template-act` | *Noted, never refused:* In an App none of whose Acts runs an authored `workflow`, every html, excel or word template is made by an Act (`template`, `templates`) or filled by an Export \u2014 the note names the app over the table whose fields it prints most, where the Act belongs. |\n| `model.filter` | A filter \u2014 a view\'s, a rollup\'s \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares. |\n\n<!-- generated:end rules-model -->\n\n## Top level\n\nEvery table of keys on this page is generated from the grammar a table document and an app tree are written in, so it is the whole of what a key may hold.\n\n<!-- generated:start top-level -->\n\n#### set_tables call\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tables` | list of [Table item](#table-item) | yes | One or more, all or nothing; each item is a table document with these keys added. It creates a table (a `label` and `fields` at least) or, with `table_id`, patches one: the document it states merges into the table\'s own |\n| `rows` | map of alias \u2192 list of [Row](#row) | no | First records of the tables the call creates, keyed by each one\'s `ref` |\n| `force` | boolean | no | Consent to losing what rows hold: a field deleted, an option removed |\n| `dry_run` | boolean | no | Answer what the call would change, writing nothing: for reading the grants a write would make or deciding `force`. A refused call writes nothing too and lists every reason, so a dry run checks no more than the call does |\n\n#### Table item\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | alias | no | A created table\'s handle in this call: other items link to it, and `rows` are keyed by it |\n| `table_id` | text | no | The table to patch; absent, the item creates one. A patch states only what changes: `null` removes a key, a list replaces whole |\n\n<!-- generated:end top-level -->\n\n**A `set_tables` item is a table document** (\xA7 Tables) with `ref` and `table_id` added, and the call\ntakes no other key. **A `set_app` body is an app tree** (\xA7 App tree), whose root is an App node: its `tables` and\n`templates` each map a local alias the tree names to the id of the table or document template it means, from the\n`set_tables` answer or `get_table`. The tree is the only home of the app\'s `name`, `description`, `icon` and `color`;\n`set_app` takes it whole to create, or as a merge patch over the App node with `app_id`, or `nodes` with `app_id` to change one, and with `base_version_id` (the\nversion you read) it refuses a write over a newer one. Nothing is built code. An unknown key is an error, never ignored.\n\nA smallest call, two tables of plain fields, a select and a two-way link \u2014 nothing else is needed to write it:\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": {\n "name": {"type": "text", "label": "Name", "required": true},\n "tier": {"type": "select", "label": "Tier", "options": [{"alias": "gold", "label": "Gold", "color": "yellow"}, {"alias": "standard", "label": "Standard", "color": "gray"}]},\n "orders": {"type": "link", "label": "Orders", "table": "order", "multi": true, "pair": "customer"}\n }\n },\n {\n "ref": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": {\n "code": {"type": "text", "label": "Code", "required": true},\n "customer": {"type": "link", "label": "Customer", "table": "customer", "multi": false, "pair": "orders"},\n "amount": {"type": "number", "label": "Amount"}\n }\n }\n ]\n}\n```\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how a document and an app tree\nname a field, an option, a view or a table; they are never shown to anyone. `label` is\nwhat a person sees.\n\nLabels must be unique within their namespace \u2014 two fields on one table, two options\non one field, two views on one table cannot share a label.\n\n## Tables\n\n<!-- generated:start table -->\n\n#### Table document\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | yes | The table\'s name in the workspace |\n| `singular` | text | no | One row of this table, in the business\'s own words \u2014 what a create\'s button and panel name |\n| `description` | text | no | The table\'s description, written onto the table in the workspace, where a caller of an app\'s queries and workflows reads it beside them |\n| `fields` | map of alias \u2192 [Field](#field) | yes | The table\'s columns, each under its alias, which is its key here and so is never stated inside it |\n| `read_scope` | [Read scope rule](#read-scope-rule) | no | Which rows a member reads. Absent, every member with access to the table reads every row. |\n| `unique` | list (at least one) of list of alias (at least one) | no | Sets of fields whose values no two live rows share \u2014 each a list of field aliases (text, number, date, a single select, or a link holding one row \u2014 `multi: false`); a single text field states `unique: true` on itself instead of a one-field set. A create or update landing a second row with the same values is refused. |\n| `views` | list of [View](#view) | no | Saved views, in the order they are listed; with none, the table still opens on its default grid |\n| `title` | alias | no | The alias of the field that names a row wherever a link or a record shows one; absent, the first text field |\n| `colors` | any value | no | Row colors: a list of { filters, color }, the filter reading fields of this table by alias and a select\'s options by alias |\n| `records` | any value | no | How a row is recognised: the keys of the Record, Status and Cadence tables below but `title`, which sits above; a table they name is an id or a `ref`. A status `history` that names a `ref` no item of the call declares is made by the call as a table, `dates` add each date field they name, and a status with stages each stage and each open option they name none for its `<option>_at`, unless the document declares it; each is listed in the result\'s notes |\n| `natural_key` | list of alias (at least one) | no | The field aliases a row of this table is RECOGNISED by: an app\'s add and find-or-create reuse the row they find by these values, minting one only where nothing matches. A record tool or the API creates the row it is handed. |\n| `no_overlap` | [No overlap](#no-overlap) | no | No two rows hold overlapping spans \u2014 a create or update that would is refused, and so is a change to the dates or states of the row a span is read through that would make two rows overlap |\n| `checks` | map of alias \u2192 table check (`workflows/table-checks`) | no | The conditions a write of the table\'s rows may not meet, each under its alias: a merge patch edits one, and `null` removes it. Expressions name fields by alias |\n| `workflows` | map of alias \u2192 table workflow (`workflows/table-workflows`) | no | The effects that follow every committed write of the table\'s rows \u2014 the grid\'s, an import\'s, an agent\'s, any app\'s \u2014 each under its alias: a merge patch edits one, and `null` removes it. A body names fields, options and tables by alias |\n\n<!-- generated:end table -->\n\n<!-- generated:start rules-entity -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `entity.singular` | A table a List\'s `create` is mounted over \u2014 the register or a child List that adds rows \u2014 states its `singular`: one row of it, in the business\'s words. |\n| `entity.required-cycle` | Required links never wait on each other in a loop: no row of the loop could be created first. |\n| `entity.link-format` | A text field of `format: "link"` holds web addresses; one whose rows hold a phone or a mail address states no format. |\n| `entity.read-scope` | A `read_scope` clause names a group (`member_of`) or a field of its table reached through one-row links: `"self"` a member field, options a single select declares; an `any` or an `all` groups clauses, and groups nest. |\n| `entity.unique` | A `unique` set names two fields or more of its table once each, each holding one value to compare \u2014 text, a number, a date, a single select, a one-row link \u2014 and is stated once; one field alone is `unique: true` on it. |\n\n<!-- generated:end rules-entity -->\n\n**`unique` is a set of values no two live rows share.** Each entry names fields\nof this table holding ONE value \u2014 text, number, date, a single select, a link\nwith `multi: false` \u2014 and a create or update landing a second row with the\nsame values is refused. A set of one text field is that field\'s own `unique:\ntrue`, so it is refused here. A create carries each set in the names its panel\nsends, so the panel can name the duplicate before the write does.\n\n**`singular` is what a create says** \u2014 `New Order`, `Add Claim line`, `H\u1ED3 s\u01A1\nm\u1EDBi` \u2014 while `label` names the table, so without it the button reads `New\nOrders`. Nothing derives it: English plurals are irregular, and no language is\nexempt. In a language without plural forms it is usually the label itself,\nless any word for the collection. A List that leaves `create` to its default adds nothing over a table that states\nnone (nor does the app\'s `words` state one); a `create` list stated over such a table is refused (`entity.singular`).\n\n**An app that only reads a table\'s rows draws them with `edit: false`** and states `create: false` and `remove: false` \u2014 a log of what the system sent, a copy of\nwhat another system holds. The app keeps the section and opens each row at rest, with no Add and no Delete; a party\nof it is picked, never found or minted; and the app writes no create, update or remove for it. It shapes that app, not who may write: a direct write \u2014 the records\nAPI, the chat agent, an import \u2014 follows the table\'s access, as on any table, and another app over the same rows\nstates its own tree.\n\n**`read_scope`** \u2014 which rows a member reads \u2014 is its own page, `model/read-scope`.\n\n## Read scope\n\n<!-- generated:start read-scope -->\n\n#### Read scope rule\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `any` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option) \\| [Read scope rule](#read-scope-rule)) (at least one) | no | A row is readable when ANY of these holds |\n| `all` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option) \\| [Read scope rule](#read-scope-rule)) (at least one) | no | A row is readable when ALL of these hold |\n\n#### Read scope by role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `member_of` | text | yes | The id of a group (from Settings, or chat): whoever is in it reads the row |\n\n#### Read scope by member\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | link aliases from this entity outward, each holding one row (`multi: false`) \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A member field alias on this entity, or on the entity `through` lands on |\n| `is` | `"self"` | yes | The members this column names on a row read that row |\n\n#### Read scope by option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | link aliases from this entity outward, each holding one row (`multi: false`) \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A single-select field alias on this entity, or on the entity `through` lands on |\n| `is` | list of alias (at least one) | yes | Its option aliases whose rows are readable \u2014 naming none would hide every row |\n\n<!-- generated:end read-scope -->\n\n**`read_scope` is a ROW rule, enforced by the platform.** It is resolved into the table\'s own row\nfilters, so an app, a workflow reading for a viewer, and the API all answer the same rows \u2014 a per-record\nvisibility field the app merely honours is a convention, not a gate. A `"self"` clause reads a column\nof one member or several, and every group and option a clause names must exist.\n\n**An app reads every table under the table\'s own `read_scope`**, and a tree has no key to lift it. A desk whose audience\nits sharing already decides is let into the rule by a group that holds that audience (`member_of`), stated on the\ntable.\n\n**A row rule is never inherited.** A child read under a private record states\nits own: the record\'s `member_of` clauses as they are, and its column clauses\n`through` the one-row link the child hangs by \u2014 over one hop or several, each a\nlink with `"multi": false`, since over several rows the rule would admit a\nreader any one of them admits while the editor\'s guard reads the first. Only the\nstatus history takes its record\'s rule.\n\n## Field\n\nEvery field carries the keys below, and its `type`\'s section adds the rest; a\ntype whose section names no `default` takes none. `label` may not contain `{` or\n`}` (formulas reference fields by label at the platform level). Every row in `rows` states each\n`required` field it carries (neither a default nor a `default_from` copy is applied to them), and a required\nLINK is written with its row: the table it names is created first, and tables\nwhose required links name each other are refused, since none of their rows could\never be created.\n\n**A table states who keys nothing.** Whether a person may change a field in an app is that app\'s statement\n(what its tree draws, and `edit: false`, \xA7 App tree): a post\'s id a publishing workflow brings back, a count a sync fills, a status an act moves\nare left out of the apps that do not save them, and written by a workflow, the records API, the chat agent and an\nimport as the table\'s access allows. A formula, a rollup, a lookup and an autonumber are no one\'s to key already.\n\n<!-- generated:start field -->\n\n#### Field\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"date"` \\| `"boolean"` \\| `"select"` \\| `"member"` \\| `"link"` \\| `"files"` \\| `"formula"` \\| `"rollup"` \\| `"lookup"` \\| `"autonumber"` | yes | What the field holds \u2014 each type takes the further keys its own section lists |\n| `label` | text | yes | The field\'s name in the workspace |\n| `description` | text | no | The field\'s description, written onto the field in the workspace |\n| `required` | boolean | no | Refuse a record whose cell for this field is empty. Every write path \u2014 create, update, an agent\'s tool call, a workflow\'s set \u2014 refuses the row by field name. A stored field is flagged only once every row holds a value. |\n\n<!-- generated:end field -->\n\n### `text`\n\n<!-- generated:start field-text -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `unique` | boolean | no | Unique values required |\n| `format` | `"text"` \\| `"link"` \\| `"markdown"` | no | How the words are drawn \u2014 plain, as a link that opens, or as markdown |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `suggest` | [Suggest](#suggest) | no | A text field whose control offers, as the reader types, the distinct values a text field of a catalog table holds (a damage position\'s code among the codes on file); any text is still written |\n\n<!-- generated:end field-text -->\n\n### `number`\n\n<!-- generated:start field-number -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | number | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. Omitted, a decimal. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (its notation\'s `per_row`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n\n<!-- generated:end field-number -->\n\n`notation` is how the number reads, and every surface reads it:\n`{ "style": "currency", "currency": "USD" }` prints as money in its code,\n`{ "style": "unit", "unit": "kg" }` as a quantity with its unit after it,\n`{ "style": "unit", "unit": "percent" }` as a percent \u2014 10 is "10%", never\nmultiplied \u2014 and `{ "style": "decimal" }`, or no notation, as a plain number. One\nkey, so a figure reads one way.\n\nAn absent number is drawn blank, except a `sum` or count rollup read as a meter\'s measure: nothing accumulated yet reads 0.\n\nA unit \u2014 `{ "style": "unit", "unit": "kg" }` \u2014 is what a figure counts or\nmeasures; `percent` aside, it is measured or counted. A **measured** unit is a code of one catalog \u2014 mass `g`\n`kg` `t`, volume `l` `m3` `cbm` (a cubic metre under freight\'s name), length\n`mm` `cm` `m` `km`, area `m2`, duration `min` `h` `day` \u2014 so a figure typed in\nanother unit of its dimension converts (`12,5 t` into a `kg` field is 12 500),\nand a tile or a chart reads it in the largest unit it reaches (12 500 kg as\n12,5 t\u1EA5n; CBM never scales) while a cell, a fact and a column always read in the\nfield\'s own. Any other noun of at most 12 characters (`ki\u1EC7n`, `pallet`, `TEU`) is a\n**counted** unit: a word after the figure that never converts. A measured unit is\nwritten as its code \u2014 `t\u1EA5n`, `KG` or `m\xB3` is refused, naming the code. A formula\nstates its own as its `notation`; a rollup that keeps the value (`sum`, `avg`,\n`median`, `min`, `max`, `range`) and a lookup carry the unit of the figure they\nread, exactly as they carry a currency, and a count carries none. A quantity is a\nnumber with its unit, never words (`3 cartons` in a text field): only a number\nsums, converts and reads down a column.\n\nA field\'s cells always hold figures in its own unit, so moving it between two\nunits of one dimension (`kg` to `t`) converts every stored figure \u2014 a change made\nin the workspace, in the field\'s settings (which say how many first) or through\n`set_tables`. Any other change of unit\nrelabels.\n\nA figure whose unit or currency varies by row names a single select of its own\nrow as `per_row` \u2014 `{ "style": "unit", "unit": { "per_row": "uom" } }`,\n`{ "style": "currency", "currency": { "per_row": "currency" } }` \u2014 or a lookup of\none through a one-link, a line reading its shipment\'s currency. That select\'s\noptions are the vocabulary: each label a unit (`chi\u1EBFc`, `kg`), or an ISO 4217 code\n(`USD`). A label outside it is refused when the figure is written and when the\nselect is \u2014 its options, its type or its deletion while a figure names it. A row\nwhose select is empty reads its figure bare. Every app reads each row\'s figure in\nits own row\'s unit, and a sum never mixes units: totals are one per unit (past three units a total the runtime draws over a set of rows \u2014\nsummary line, group totals and feet, a dashboard\'s table foot \u2014\nstates the count alone; a reading an author states draws every unit), a chart draws one at a time. A rollup that keeps the value (`sum`, `avg`, `min`\u2026)\nover such a figure stands only where the child\'s select is a lookup, through\nthe rollup\'s own link, of a select on this table \u2014 the rollup reads that\nselect\'s unit; otherwise it is refused. A lookup of such a figure stands only\nwhere its table also looks up the figure\'s select through the same one-link,\nand reads in that lookup; otherwise it is refused. A count is unaffected. Moving a field between a\nfixed and a per-row unit relabels.\n\n### `date`\n\n<!-- generated:start field-date -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. A date string in the field\'s format. |\n| `format` | `"date"` \\| `"datetime"` \\| `"date_range"` \\| `"datetime_range"` | no | Whether the field holds a day or a moment, alone or as a span |\n| `timezone` | text | no | IANA timezone |\n| `derive_from` | `"created_at"` \\| `"updated_at"` | no | Auto-populate from the row\'s system timestamp; the field becomes read-only. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (its notation\'s `per_row`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n\n<!-- generated:end field-date -->\n\nA `default` is refused beside `derive_from`: the platform stamps that date.\n\n### `boolean`\n\n<!-- generated:start field-boolean -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | boolean | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-boolean -->\n\n### `select`\n\n<!-- generated:start field-select -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | list of alias | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. Option alias(es) this field declares \u2014 one for single-select. |\n| `options` | list of [Select option](#select-option) (at least one) | yes | The choices, in the order every picker and every ladder lists them |\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n#### Select option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable local alias, unique within the field |\n| `label` | text | yes | Display label for the option |\n| `color` | [colour](#select) | no | The colour the option\'s badge is drawn in; absent, the platform gives it one no sibling option holds |\n| `mark` | [mark](#select) | no | The option\'s own mark, drawn in place of its colour dot wherever the option is shown: the brand it is ({kind: "brand", name: one of facebook, instagram, threads, meta, tiktok, google-ads, zalo, linkedin, x, google-meet, youtube, telegram, whatsapp, gmail, google-drive, outlook, kiotviet, misa, lark, payos}) a kit glyph ({kind: "icon", name: "wrench"}) or the face of who it is, drawn from its label ({kind: "avatar", name: "person" or "organization"}). Every option of a field has one, or none does. A mark a reader does not draw falls back to the dot |\n\n<!-- generated:end field-select -->\n\nA patch of a select states its whole `options` list: a list replaces whole, so an option it leaves out is deleted. To\nrename or recolour one option, restate every option kept, each with its alias. Rows still holding a deleted option refuse\nthe patch unless `force`.\n\n```json\n{\n "options": [\n { "alias": "draft", "label": "Draft" },\n { "alias": "confirmed", "label": "Confirmed" },\n { "alias": "shipped", "label": "Shipped", "color": "green" }\n ]\n}\n```\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\nEvery option is drawn in its colour wherever it shows \u2014 a cell, a row\'s line,\na record, a picker, the bar a reading splits the rows by.\n\nAn option may carry its own `mark`, drawn in place of its colour dot wherever\nthe option is shown \u2014 a stage, a chip, a filter, a fact, an entry of a log, a\nreading\'s part, a lookup of the select on another table. Where the select is its\nrows\' `records` `mark`, a brand or glyph stands on the row\'s lead tile in the option\'s\ncolour, and an option with none leads with its colour dot there. A select a reader scans\ndown a column \u2014 how a payment was made, the channel, the mode \u2014 states one on every\noption, since a glyph reads before its word: the brand it IS\n(`{"kind": "brand", "name": "tiktok"}` \u2014 one of `facebook`, `instagram`,\n`threads`, `meta`, `tiktok`, `google-ads`, `zalo`, `linkedin`, `x`,\n`google-meet`, `youtube`, `telegram`, `whatsapp`, `gmail`, `google-drive`,\n`outlook`, `kiotviet`, `misa`, `lark`, `payos`), a glyph the kit draws\n(`{"kind": "icon", "name": "wrench"}`; any other name is refused), or \u2014 where the\noptions name people or organisations who are no member and no row \u2014 the face of who\nit is, drawn from its label (`{"kind": "avatar", "name": "person"}` or\n`"organization"`). Every option of a select has one, or none does: a run of chips\nwhere one carries no mark reads as the one missing something. `set_tables` writes the\nmarks onto the table. The glyph names are `model/field/option-glyphs`.\n\n```jsonc\n"options": [\n { "alias": "short_video", "label": "Short video", "color": "zinc", "mark": { "kind": "brand", "name": "tiktok" } },\n { "alias": "print", "label": "Print", "color": "amber", "mark": { "kind": "icon", "name": "newspaper" } }\n]\n```\n\n### Option glyphs\n\nThe kit\'s glyph names an option\'s `mark` takes:\n\n<!-- generated:start field-select-glyphs -->\n\nactivity, align-center, align-left, align-right, align-vertical-justify-center, align-vertical-justify-end, align-vertical-justify-start, arrow-down, arrow-down-a-z, arrow-down-up, arrow-down-wide-narrow, arrow-left, arrow-left-from-line, arrow-right, arrow-right-from-line, arrow-right-left, arrow-up, arrow-up-down, arrow-up-wide-narrow, arrow-up-z-a, ban, banknote, bed, bell, between-horizontal-start, between-vertical-start, bold, bolt, book-marked, book-open, book-text, bot, box, brackets, brain, briefcase, building-2, calculator, calendar, calendar-clock, calendar-off, camera, car, chart-column, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down-up, chevrons-up-down, circle-alert, circle-check, circle-x, clipboard-list, clipboard-paste, clock, code, code-xml, columns-3, columns-3-cog, combine, construction, container, copy, credit-card, database, decimals-arrow-left, decimals-arrow-right, dollar-sign, download, ellipsis, eraser, expand, external-link, eye, eye-off, facebook, file, file-csv, file-down, file-question, file-spreadsheet, file-stack, file-text, file-up, folder, folder-closed, folder-open, folder-pen, form, funnel-plus, funnel-x, gauge, globe, gpu, grid-2x2, grip-vertical, group, hand-coins, hash, heading, heading-1, heading-2, heading-3, history, house, image, inbox, info, instagram, italic, keyboard, languages, layout-dashboard, layout-grid, library-big, link-2, link-2-off, linkedin, list, list-checks, list-collapse, list-filter, list-filter-plus, list-ordered, list-tree, loader, lock, lock-keyhole, lock-keyhole-open, lock-open, log-in, log-out, mail, map-pin, maximize-2, megaphone, menu, message-circle, message-circle-question-mark, message-square, messages-square, mic, minimize-2, minus, monitor, mouse, mouse-pointer-click, music, newspaper, notepad-text-dashed, package, paint-bucket, paintbrush-vertical, palette, panel-left, panel-left-close, panel-left-open, panel-right, panel-right-close, panel-right-open, paperclip, pause, pencil, percent, phone, pin, pin-off, plane, play, plug, plus, receipt, rectangle-ellipsis, redo, refresh-cw, repeat, rotate-ccw, rotate-cw, rows-3, scan, scan-line, scissors, search, send, settings, share, share-2, shield, shield-alert, shield-check, shopping-cart, sigma, sliders-horizontal, smile, smile-plus, snowflake, sparkles, split, square, square-check, square-pen, square-sigma, stethoscope, sticky-note, strikethrough, table, table-2, tag, target, text-quote, text-wrap, thumbs-down, thumbs-up, ticket, trash, trending-down, trending-up, triangle-alert, truck, tv-minimal, twitter, type, underline, undo, upload, user, user-check, user-pen, users, utensils, waypoints, workflow, wrench, x, zap, zoom-in, zoom-out\n\n<!-- generated:end field-select-glyphs -->\n\n### `member`\n\nA person picker over the workspace\'s members. No default. With no role it is still drawn \u2014\nface and name, ranked as a `party` \u2014 in a screen\'s `columns` and in the register\na record draws of these rows.\n\n<!-- generated:start field-member -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-member -->\n\n### `link`\n\n<!-- generated:start field-link -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `options_where` | [filter](#views) | no | Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET table\'s own fields, each `field_key` a field alias. It narrows every picker of the link, and a save naming a row outside it is refused on every path \u2014 an app, the grid, an import, a workflow, the API. |\n| `same` | list of alias (at least one) | no | One-row links this row and each row this link points at must name alike \u2014 a fee\'s invoice is one of its own visit\'s, a box\'s seal one of its own shipping line\'s: each a one-row link of both tables to the same table. It narrows the picker\'s read to the rows naming what this row names, a row made from the picker names it too (set, never asked), and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare. |\n| `table` | text | yes | The table the link points at: its alias, or the `ref` of another item of the call |\n| `multi` | boolean | yes | true: the link holds many rows; false: one. Where the link is paired, at most one of the two sides holds one |\n| `pair` | `true` \\| alias | no | Makes the link two-way: the alias of its partner field on the other table, which names this one back, or `true` to have the alias derived. Stated on one table, the partner is made on the other with the inverse cardinality; a many-to-many link states both sides |\n\n<!-- generated:end field-link -->\n\n**`multi` is stated on every link:** `true` holds several rows, `false` one \u2014 one customer on an\ninvoice, one project on a device. There is no default.\n\nA two-way link states `pair` on one table, as `true` or a free alias, and the partner is made on the\nother table with the inverse cardinality; a many-to-many link states both sides, since the inverse of\nmany is not decided. A pair stated on both sides is made once. Both tables are written in the one\ncall (`table` names the other item\'s `ref`). A link with no `pair` is one-way, and nothing is created on the target. Pair a link when\nthe target\'s own record should list what points at it, and leave it unpaired when it should not: either\nway the record plan draws the relation as a section on the side it points at, so an unpaired link costs\nthe target nothing. Of two paired links at most one holds one row.\n\n```jsonc\n// a field of table "order"\n"customer": { "type": "link", "label": "Customer", "table": "customer", "multi": false, "pair": "orders" }\n```\n\n```jsonc\n// a field of table "customer"\n"orders": { "type": "link", "label": "Orders", "table": "order", "multi": true, "pair": "customer" }\n```\n\n### `files`\n\nA row attaches documents to it (\xA7 Rows). Each file\nis drawn by its kind, with no key to choose: a picture (an image, a video) as its\nthumbnail, a document (a PDF, a sheet) as its type\'s badge and its filename.\n\n### `formula`\n\nA formula states its keys on the field itself, beside `type` and `label`:\n`{ "type": "formula", "label": "Total", "expression": "{quantity} * {unit_price}", "notation": { "style": "currency", "currency": "USD" } }`.\n\n<!-- generated:start field-formula -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `expression` | text | yes | The expression, over fields of THIS entity by alias in braces \u2014 `{quantity} * {unit_price}` |\n| `output_type` | `"number"` \\| `"text"` \\| `"date"` \\| `"datetime"` \\| `"boolean"` \\| `"select"` | no | Absent, it is what the expression yields. Stated, it is checked against that and refused where they differ (a datetime is never stated date) |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. For a formula yielding a number. |\n| `link` | `true` | no | The formula yields a URL as text, drawn as a link that opens. |\n| `options` | list of [Select option](#select-option) (at least one) | no | The categories the formula yields, drawn as a single select\'s options are (read-only). The expression yields one of them as `{this_field:option}`, or null \u2014 `{days_idle} > 30 ? {warmth:cold} : {warmth:hot}`. Omit for a formula yielding a plain value. |\n\n<!-- generated:end field-formula -->\n\n`output_type` is what lets a role or a screen clause accept a computed value: a\ncaption over a derived name (`output_type: "text"`), a period over a settled date\n(`"date"`). A formula declaring no `output_type`, `notation` or `link` says nothing\nabout its result, and every rule that needs one refuses it by name. A formula stating\n`options` is a computed category: read-only, and read as a single select\nwherever it is drawn \u2014 a column, a filter, a reading\'s `by` or `where`.\n\n**A select reaches a formula as the KEYS of its chosen options**, a list \u2014\nnever their labels, and never their aliases \u2014 and a document has no keys: the\nworkspace mints them when the table is made. So an option is named in a formula\nas `{field:option}`, both aliases, and the write puts that option\'s key in its\nplace: `includes({kind}, {kind:crate})` for a select holding one or several,\n`{kind}[0] == {kind:crate}` for a single one. A select compared to its own words\n(`{kind} != "Crate"`) matches no row and computes the other branch everywhere,\nso the check refuses it and names the token. A select looked up from another\ntable is tested there, in a formula of its own, and that result looked up.\n\n**The language.** The check refuses a call or a name the platform cannot run, naming what to write:\n\n<!-- generated:start field-formula-language -->\n\n- Operators: `+ - * / %`, `== != > < >= <=`, `&& || !`; `+` also joins text\n- Numbers: arithmetic is exact decimal (`0.1 * 3` is `0.3`), a division that does not end the nearest double; a comparison between two numbers reads both at 15 significant digits; `round` rounds a half away from zero (`round(-2.5)` is `-3`)\n- Conditionals: a ternary only \u2014 `{amount} > 100 ? "High" : "Low"`\n- Not supported: optional chaining (`?.`), nullish coalescing (`??`), template literals, arrow functions \u2014 use `get(obj, "path", default)`, `coalesce(v1, v2)`\n- Helpers are these names, spelled exactly; a spreadsheet function (`IF`, `SUM`, `LEN`, `DATEDIF`) is none of them, and a formula calling one is refused\n- Math: round(n,decimals?), ceil(n), floor(n), abs(n), min(a,b), max(a,b), sum(arr), mean(arr), clamp(n,min,max), percentage(part,total,decimals?), pow(base,exp), sqrt(n), mod(n,divisor)\n- Strings: upper(s), lower(s), trim(s), capitalize(s), length(s), contains(s,search), join(arr,sep), split(s,sep), replace(s,search,rep), replaceAll(s,search,rep), removeChars(s,chars) (s without any character that occurs in chars), removeSpaces(s) (s without any whitespace), startsWith(s,prefix), endsWith(s,suffix), substring(s,start,end?), padStart(s,len,char), padEnd(s,len,char), numberToWords(n, lang?) (lang \'vi\' default, or \'en\'), uuidFrom(s) (the same UUID for the same text)\n- Lists: includes(list,value), first(list), last(list), unique(list), compact(list) \u2014 length(list) counts one\n- Dates: now(), formatDate(d,fmt), addDays(d,n), subDays(d,n), addHours(d,n), subHours(d,n), addMinutes(d,n), subMinutes(d,n), startOfDay(d), endOfDay(d), differenceInCalendarDays(later,earlier), differenceInHours(later,earlier), differenceInMinutes(later,earlier), isBefore(d1,d2), isAfter(d1,d2), isSameDay(d1,d2), isToday(d), isWithinRange(d,start,end), meetsBounds(value,kind,{gt,gte,lt,lte},tz?), parseDate(d)\n- Null/type: isNull(v), isEmpty(v) (also true for "" and []), coalesce(v1,v2,...), isString(v), isNumber(v), isBoolean(v), isArray(v), toNumber(v), toFigure(v), toString(v)\n- Other: formatCurrency(amount,locale,currency), formatDecimal(value,decimals,locale) (grouped quantity, no symbol), get(obj,"path",default?)\n- Empty cells: a cell nobody filled is null inside a formula, whatever its type; one holding 0, false or "0" is not empty. Test it with `isEmpty({note})` \u2014 `{note} == ""` and `{done} == false` are false on an unset cell\n- Arithmetic over an empty cell: `+` and `-` read it as 0 beside a value (`{fee} + {surcharge}` is `{fee}` when the surcharge is empty, null when both are); `*`, `/`, `%` and a unary `-` yield null (`{price} * {qty}` is null, not 0, when the quantity is empty)\n- No helper throws on an empty cell: the math helpers and toNumber return null, the string helpers "". A value of the wrong type still errors. When EVERY field a formula reads is empty it is null \u2014 unless it reads each only as the argument of isEmpty, isNull or isNotNull (`!isEmpty({file})` is false there, not null)\n\n<!-- generated:end field-formula-language -->\n\n### `rollup`\n\nAggregates the records reached through a link on this table.\n\n<!-- generated:start field-rollup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `filter` | [filter](#views) group | no | Only linked records matching this filter are aggregated; one condition on its own is a group of one. Every `field_key` in it names a field alias on the linked entity, and a select condition\'s value names an option alias there. A traversal node reaches past that entity, so its `path` hops and inner `field_key` are fully-qualified `entity.field` aliases. |\n| `via` | alias | yes | The alias of a link of this table to roll up from |\n| `op` | [operation](#rollup) | yes | The operation over the linked rows |\n| `field` | alias | no | The alias of the field on the linked table the operation reads; absent for `count` |\n\n<!-- generated:end field-rollup -->\n\nA rollup states `via` (a link of this table), `op` and \u2014 except for `count` \u2014 `field`, an alias on the\nlinked table; `op` is one of `count`,\n`sum`, `avg`, `median`, `min`, `max`, `range`, `empty`, `filled`,\n`percent_empty`, `percent_filled`, `unique`, `percent_unique`, `earliest`,\n`latest`, `date_range`, `checked`, `unchecked`, `percent_checked`,\n`percent_unchecked`. The operation must be one the aggregated field\'s type\nallows \u2014 `sum` over a number, `earliest` over a date, `filled` over any stored or\nformula field. A lookup is never rolled up: roll up the child\'s own field, or a formula\nover it. A rollup states no `notation`: `sum`, `avg`, `median`, `min`, `max` and `range`\nread in the aggregated figure\'s, a count as a plain number, and a `percent_*` as a\npercent (0\u2013100), in the app as in the table.\n\n### `lookup`\n\nDisplays a field from the linked records: `via` names a link of this table, `field` a field of the table it\npoints at, and `order_by` (`field`, `direction`) picks the one row whose value shows.\n\n<!-- generated:start field-lookup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `via` | alias | yes | The alias of a link of this table to look up through |\n| `field` | alias | yes | The alias of the field on the linked table to show |\n| `order_by` | [Lookup order](#lookup-order) | no | Show ONE linked row\'s value \u2014 the first in this order \u2014 rather than every linked row\'s |\n\n#### Lookup order\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A field alias on the linked table the rows are ordered by |\n| `direction` | `"asc"` \\| `"desc"` | yes | Which end of that order the one row is taken from |\n\n<!-- generated:end field-lookup -->\n\nInside a formula, a lookup holding one value is that value (`{due_soon}` is\n`true`, not `[true]`); several values are a list.\n\n### `autonumber`\n\n<!-- generated:start field-autonumber -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `prefix` | text | no | Literal prefix prepended to every display value (e.g. \'KH-\' \u2192 \'KH-001\'). Ignored when `template` is set. |\n| `padding` | integer | no | Zero-pad the integer to this width. Default 1 (no padding). 3 \u2192 \'001\', \'012\', \'123\', \'1234\' (overflow uses the actual width). Ignored when `template` is set. |\n| `template` | text | no | Format template with placeholder tokens evaluated at insert time. Tokens: {N} (raw integer), {N:W} (zero-padded to width W, e.g. {N:3} \u2192 001), {YEAR} (4-digit year), {YEAR:2} (2-digit year), {MONTH} (2-digit month), {DAY} (2-digit day). Date tokens use the workspace timezone. Example: \'HM-{YEAR}-{N:3}\' yields \'HM-2026-001\'. Stored as the composed string; subsequent template edits do NOT re-format existing rows (date tokens would lose the original creation date). |\n| `next` | integer | no | Sets the number the next row takes, once, as the patch is written; never below one past the highest number issued. On a field added to a table with rows, those rows are numbered from it in creation order. It is no setting of the field: a read of the table shows none |\n\n<!-- generated:end field-autonumber -->\n\nA refused write and a dry run take no number, so a table\'s numbers run without a gap; a removed row\'s number is not\nissued again.\n\n<!-- generated:start rules-field -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `field.default` | A default names options its select declares \u2014 one on a single select \u2014 and a date stamped by `derive_from` states none. |\n| `field.default-from` | A field\'s `default_from` reads `<link>.<field>` off the row a one-row link of its table names \u2014 a link no copy fills \u2014 or, through further one-row links, `<link>.<link>.<field>` off the row the last names, into a field of the same type, date format and notation that `derive_from` does not stamp: a lookup only through a one-row link, a link only into one to the same table, a select\'s option as this field\'s of the same alias (one it lacks is not copied), several values only into a field holding several. The server copies it on every add that fills the link and leaves the field empty, so a List\'s `create` sure to fill the link \u2014 a required one, or the record it files the row under \u2014 does not ask it by default. |\n| `field.option-mark` | An option\'s `mark` is one the kit draws, and a select\'s options mark every one or none. |\n| `field.unit-select` | A notation\'s `per_row` names a single select of the same row, or a lookup of one through a one-link, whose every option label is a unit, or an ISO 4217 code. |\n| `field.link` | A link targets a table of the call or the workspace: its `pair` a link on that table naming this one back \u2014 and of two paired links at most one reads one row (`multi: false`). |\n| `field.formula` | A formula parses, reads fields of its own table by alias, names an option as `{field:option}` of one a select declares, and compares a select to its options\' keys, never their words. |\n| `field.formula-type` | A formula yields what it states \u2014 its `output_type` (the type the platform stores: a `datetime` is never stated `date`), else a figure where it states a `notation` and text where it states `link` \u2014 as the platform infers it from the expression when the field is created. |\n| `field.rollup` | A rollup aggregates, through a link of its table (`via`), a field of the linked table (`field`) by an operation (`op`) that field\'s type takes \u2014 never a figure read in each row\'s own unit or currency \u2014 filtered on fields of the linked table. |\n| `field.lookup` | A lookup reads (`field`), through a link of its table (`via`), a field of the linked table \u2014 a figure read in each row\'s own unit or currency only where this table looks up that row\'s select through the same one-row link \u2014 ordered by a field of it (`order_by`). |\n| `field.computed-cycle` | Computed fields never wait on each other in a loop: each is computed after what it reads. |\n\n<!-- generated:end rules-field -->\n\n## Views\n\nSaved views live under the table they belong to. Every field reference is a\nfield ALIAS on that table. A `set_tables` patch of an existing table refuses `views`: they are stated when the table is\ncreated, and change afterwards in chat.\n\n<!-- generated:start views -->\n\n#### View\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable view alias, unique within the entity |\n| `label` | text | yes | Display name of the view |\n| `description` | text | no | The view\'s description, written onto the view in the workspace |\n| `columns` | list of [View column](#view-column) (at least one) | no | The columns the view shows, in this order and no others; absent, every field |\n| `filters` | [filter](#views) | no | The rows the view keeps |\n| `sort` | [sort](#views) | no | The order the view reads its rows in |\n| `summary` | map of text \u2192 text | no | Field alias \u2192 the operation its footer cell states |\n| `frozen_columns` | integer \\| `null` | no | How many leading columns stay in place while the rest scroll |\n\n#### View column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_alias` | alias | yes | A field of this entity |\n| `visibility` | `"visible"` \\| `"hidden"` | yes | Field visibility state: \'visible\' = shown to everyone, \'hidden\' = not shown by default but members can toggle |\n| `width` | number | no | The column\'s width, in pixels |\n\n<!-- generated:end views -->\n\nA filter is a group \u2014 `{ "node_type": "group", "logic": "and" | "or",\n"children": [ \u2026 ] }` \u2014 or one condition on its own, `{ "node_type":\n"condition", "type": "select", "field_key": "tier", "operator": "has_any_of",\n"value": ["gold"] }`. A sort is a list of `{ "field_key": \u2026, "order": "asc" |\n"desc" | null }`. A condition\'s `type` is the field\'s type and its `operator` is\none that type admits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n<!-- generated:start rules-view -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `view.fields` | A view\'s columns, summary, sort and filters name fields of its table. |\n\n<!-- generated:end rules-view -->\n\n## Groups and templates\n\nA member group and a document template are made on their own, outside a table document and an app tree. A tree names a\ntemplate by its alias. A table document names a group by its id, where a `read_scope` clause (`member_of`),\na stage owner (`group`) or a check\'s `member_of("<id>")` takes one. `get_workspace` kind `group` lists the groups with their ids and members; `set_group` creates,\nchanges or deletes one (an admin\'s verb).\n\n`{{name}}` in an `html` or `email` template is filled from the workflow\'s data. Inline content is refused where it holds a\nscript, a frame, a `<link>`, an inline event handler or a `javascript:` URL; an `html` one may be a fragment, kept as\nwritten and printed as a whole document.\n\nA template\'s markup is the `template` of `set_template`, as one JSON string. A paper shaped like a counterparty\'s \u2014 a letterhead, a reference line, a seal, a signature block \u2014 is an `html` template with a shell around the body; `document_templates` covers the types.\n\n**What an act\'s template is handed** is its record\'s own fields by alias: a\nselect, a member and a link as their words (a link as the linked row\'s title), a\nnumber, a date and a computed value that declares its kind raw (`2800000`,\n`2026-09-14`) and a files field as its file cells (`{id, name, mime, url}`, the photos in the order held; a child row\'s\nfiles likewise) \u2014 never a computed select. On one record it is\nhanded, too, the rows of each child List the record draws, listed under that child\'s\nlink\'s name for `{{#each <link>}}` (and under the child entity\'s alias too): every row filed under the record, oldest first, each\nby its own fields as the record\'s are, less its link back to the record. Over\nseveral rows the rows are listed under the table\'s alias, for\n`{{#each <alias>}}`, without their child rows.\n\n**Photos in a paper.** A file cell is drawn only if whoever makes the paper may read that file, by the rule the file\nproxy applies; a file they may not read is never drawn. html: `{{#each photos}}<img src="{{url}}">{{/each}}`, each\nphoto inline, only those the data\'s file cells carry (a url typed into a text field stays a link). Word:\n`{{IMAGE picture(cover, 8)}}` for one file cell, and `{{FOR p IN photos}}{{IMAGE picture($p, 8)}}{{END-FOR p}}` for\nmany. Excel: `{{cover | picture:6}}` as the whole cell, and `{{#each photos}}` with `{{this | picture}}` for many, each\nrow growing to its picture. The number is the width in centimetres (8 by default, 6 in Excel); the height follows the\npicture. `picture` takes one image file cell: a list, a file that is no image and a file that cannot be read are\nrefused by name. Photos are embedded at most 1600 pixels across.\n\nHelpers print a raw value in the template\'s `locale` \u2014 `vi` or `en`, the organization\'s language where it states\nnone; each prints `""` for an empty value:\n\n| Helper | Prints (`vi` / `en`) |\n|---|---|\n| `{{money total "USD"}}` | `1.250,50 US$` / `$1,250.50`, in the currency\'s minor units; no code, a number |\n| `{{number qty}}` | `1.234,5` / `1,234.5`: grouped, up to 2 decimals |\n| `{{date issued_on}}` | `14/09/2026`; a date-fns pattern as the second argument, `{{date at "dd/MM/yyyy HH:mm"}}`, literal text quoted (`"\'Ng\xE0y\' dd"`). A moment prints on the workspace\'s timezone |\n| `{{words total "USD"}}` | `M\u1ED9t ngh\xECn hai tr\u0103m n\u0103m m\u01B0\u01A1i \u0111\xF4 la M\u1EF9 n\u0103m m\u01B0\u01A1i xu` / `One thousand two hundred fifty US dollars and fifty cents`; no code, VND (`\u0111\u1ED3ng`) |\n| `{{qr link}}`, `{{code128 position 6}}` | the value as a QR code, or as a Code 128 barcode (ASCII text only), a picture the width of the optional second argument in centimetres, by default sized to the value (a module prints 0.5 mm in a QR code, 0.33 mm in a barcode); a width too narrow to scan is refused, naming the least that does; not in an email template |\n| `{{date (add_days issued_on -1)}}` | `13/09/2026`: the date shifted by whole days (`-1`: the day before), for `date` to print; a number field as the days, empty shifting none |\n\nA value it cannot read (`{{money "abc"}}`, `{{date "14/09/2026"}}`) fails the render. A field named like a\nhelper and written alone (`{{number}}`) prints its own value.\n\nA field printed as a code in the other types: Excel `{{position | qr}}` or `{{position | code128:6}}` as the whole\ncell and the last helper \u2014 the picture is anchored on the cell and its row grows to hold it; Word `{{IMAGE qr(position)}}` or `{{IMAGE code128(position, 6)}}`. An empty field draws no picture.\n\n## Rows\n\n`rows` of a `set_tables` call are the first records of the tables the call creates, keyed by each one\'s `ref`;\na patch of an existing table seeds none, so write its rows into the table. A sample, not a data set: rows past it go in with `create_records` once the tables exist, in the batches its description bounds.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n`rows` sits beside `tables` in the call, not inside a table.\n\n<!-- generated:start rows -->\n\n#### Row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | text | yes | Local handle for this row, referenced by other rows\' link fields |\n| `fields` | map of text \u2192 any value | yes | Field alias \u2192 the value, read against the field\'s declared type |\n\n<!-- generated:end rows -->\n\n<!-- generated:start rules-rows -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `rows.ref` | A row\'s `ref` is unique within its table. |\n| `rows.required` | A row states every field its table requires: it is created with what it states, its fields\' defaults and copies unapplied. |\n| `rows.value` | A row\'s value fits its field: an option alias for a select, `"<table ref>:<row ref>"` of a row of the linked table in this call for a link, `"self"` for a member, a `fil_` id for files, a date in the date grammar (an hour only on a datetime), a text, number, yes/no or null otherwise \u2014 and none on a computed field. |\n\n<!-- generated:end rules-rows -->\n\nA `ref` is lowercase letters, digits and underscores, and is never persisted.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `link` | `"<table ref>:<row ref>"` naming another row of this call \u2014 `"customer:acme"`, or an array for several; a paired link is stated on ONE side (the child\'s link to its parent) and its partner fills itself |\n| `member` | `"self"` only \u2014 the member making the call |\n| `files` | the `fil_` id of a file already in this workspace, or a list of them |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe call is made, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n- on a date that holds its hour (`format: "datetime"`), that hour after the day, and always stated:\n `@today 14:30`, `@today+1 06:00`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Records\n\n`records` says how a row of a table is RECOGNISED \u2014 stated once in the table\'s document, beside its\n`title` \u2014 and every surface that draws one of its rows reads the same\nstatement: the register\'s row, a picker\'s option, a link\'s chip, a child table\'s\nrow and the record\'s header. Every table an app lists, opens or picks has one. The rarer keys \u2014 `scan`, `cadence`, `place`, `on_site`, `pin`, `plan_place` and `size` \u2014 are stated on `model/record-places-pins-and-cadence`.\n\n```jsonc\n// table "visit"\n"title": "container_no",\n"records": {\n "subtitle": ["customer", "arrived_on"],\n "image": "photos",\n "status": { "field": "stage", "closed": ["gone"], "history": "visit_history" },\n "figure": "total_fees",\n "due": ["free_until"]\n}\n```\n\n```jsonc\n// table "release": a release order read against what it allows \u2014 a meter wherever it shows\n"title": "release_no",\n"records": { "figure": "issued", "limits": { "issued": "units" } }\n```\n\n```jsonc\n// table "dossier": a straight-line process \u2014 the stage a select, each step\'s day the date it stamps\n"title": "applicant",\n"records": { "status": { "field": "stage", "closed": ["signed"], "dates": { "received": "received_on", "appraised": "appraised_on", "signed": "signed_on" } } }\n```\n\n<!-- generated:start records -->\n\n#### Record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title wherever a row is drawn (a code, a date, a party): one line at a desk, wrapping to a second on a phone |\n| `image` | alias | no | A files field that pictures the row \u2014 never one an act keeps the document it makes in |\n| `mark` | alias | no | A single select whose option leads the row wherever a mark leads it \u2014 a list\'s row, the record\'s head, a link naming the row: the option\'s brand or glyph, else its colour dot, on a tile. The field stays a field: a subtitle run, a column, a section\'s control. Never beside `image` or `party`, and never the status field |\n| `description` | alias | no | A text field saying what the row is about \u2014 a plan\'s brief, a customer\'s note: read in its record\'s head under the title\'s line, its first lines at rest and whole on a press, and written from the head\'s \u270E. Never on a row\'s line |\n| `party` | `"person"` \\| `"organization"` | no | What each row is where the rows are people (a contact, a patient) or organisations (a customer, a supplier, a carrier); absent, the rows are things (an order, an item, a paper) |\n| `status` | [Status](#status) | no | The stage a row moves through, a single select, drawn as a badge beside its title |\n| `figure` | alias | no | The one number the row stands for, read at its right (a total, a quantity left) |\n| `limits` | map of alias \u2192 (alias \\| number) | no | A number read against a bound \u2014 a field of the same row or a constant \u2014 drawn as a meter wherever it shows |\n| `gates` | map of alias \u2192 (alias \\| number) | no | A pass mark on a meter `limits` bounds \u2014 a field of the same row or a constant; a percent field is its share of the bound, any other number an amount. The meter marks it and fills complete once past it |\n| `tolerance` | map of alias \u2192 number | no | How far past its `limits` bound a meter may stand, in percent of the bound: the bound is an amount expected (received against ordered), not a cap. Without one the bound is a cap, and past it is an overrun |\n| `due` | list of alias (at least one) | no | Dates that are deadlines (stored, or computed as a date): each counts down and turns overdue |\n| `frees` | alias | no | A date ending the row\'s span of days on which what the row holds is free again (a check-out, a hire\'s return): a stay from the 3rd to the 5th holds the 3rd and the 4th, and another may start on the 5th \u2014 on a lanes board, a calendar, a roster and in `no_overlap` alike. Absent, a span of days holds its last day; a span of moments always frees its end |\n| `settled` | alias | no | A yes/no of the row (stored, a formula: an invoice paid in full, or looked up through a one-row link: a line settled with its parent) that ends its deadlines: while it is true, no `due` of the row counts down or turns overdue \u2014 in a cell, on its line, in its head, on a calendar or gantt, or in the overdue count. A dashboard\'s or reading\'s `where` reads it as any other yes/no |\n| `task` | `true` | no | A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record\'s rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed` |\n| `applies` | map of alias \u2192 map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Fields that apply only while their conditions hold (a length only on a line whose tariff charges by the metre): where one does not apply no add asks it and no surface shows it. Never the title; a required one starts at its `default` |\n| `starts` | map of alias \u2192 `"today"` \\| `"me"` | no | What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default` |\n\n<!-- generated:end records -->\n\n<!-- generated:start rules-records -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.title` | A title is the one field naming the row \u2014 any field but a button or a yes/no \u2014 and the subtitle never names the title\'s own field. |\n| `records.image` | `image` is a files field, never one an Act keeps the documents it makes in (`into`). |\n| `records.mark` | `mark` is a single select, never the status field, and never beside `image` or `party` \u2014 a List keeps one lead slot \u2014 nor on the folder an App works inside (`scope`), which its switcher names by a picture or its initials. |\n| `records.description` | `description` is a text field, read in the record\'s head \u2014 never a Field of a part too. |\n| `records.figure` | A figure is a number. |\n| `records.figure-on-line` | *Noted, never refused:* A subtitle entry equal to the figure is shown once, as the headline; the line leaves it out. |\n| `records.applies` | `applies` holds a field of the row other than its title to conditions of the same row \u2014 a yes/no (stored or a formula) by `true` or `false`, a single select by its options, its own or looked up through a one-row link \u2014 never to itself; a required field it holds starts at a `default`. |\n| `records.limits` | `limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit. |\n| `records.gates` | `gates` marks a pass on a meter `limits` bounds: a percent field (its share of the bound), another number in the bound\'s unit, or a constant. |\n| `records.tolerance` | `tolerance` stands on a meter `limits` bounds. |\n| `records.starts` | `starts` starts a field a person writes \u2014 `today` a date or a moment (never a span), `me` a member \u2014 and a `create` that does not ask a field it starts writes that start. |\n| `records.due` | A deadline (`due`) is a date. |\n| `records.frees` | `frees` names a stored day, never a moment: a span of moments already frees its end. |\n| `records.settled` | `settled` names a yes/no of the row (stored, a formula, or looked up through a one-row link) beside a `due`: while it holds, the row\'s deadlines neither count down nor turn overdue. |\n| `records.task` | A task entity (`task`) states a `status` of a stored single select with `closed` options \u2014 its ring ticks a row done by moving it to one. |\n| `records.picture` | *Noted by the designer on a write, never refused:* Rows an App draws that each hold one files field no Act makes documents into, no call is filed into, no files gallery keeps and no List compares as a pair (`compare`) \u2014 and no figure \u2014 are pictured by it (`image`); where the workspace\'s files are known, the field holding images is the picture, a figure or not. |\n\n<!-- generated:end rules-records -->\n\n- **The title names the row** by who or what it is \u2014 a link by the linked row (a job\n read by its customer). Any field but a button or a yes/no may title it, yet a code\n titles a row only where people name the row by it, else it is a `subtitle`; it is the table\'s own setting, which every link shows.\n- **One lead per row.** A row leads with its picture (`image`), a face (`party`) or the\n option of a single select it holds (`mark`) \u2014 never two. A `mark` draws that option\n on a tile: its brand or glyph, else its colour dot; a row holding none, a ring saying\n none is picked. The same tile heads the row\'s record and leads it where a link\n names it; the select stays a field \u2014 a subtitle run, a column, a section\'s control.\n- **A limit** reads a number against a bound \u2014 another field of the row, or a constant.\n A `gates` entry marks a pass on that meter: a percent field is its share of the\n bound, any other number an amount.\n- **A deadline** (`due`) counts down wherever it is drawn and is overdue once past while\n the work is open. A register over the table says how many rows in view are overdue on\n its summary line, and pressing that count narrows to them \u2014 no reading to state.\n- **A span\'s free day** (`frees`) is the end date on which what the row held is free\n again \u2014 a check-out, a hire\'s return. A stay from the 3rd to the 5th holds two nights:\n a lanes board, a calendar and a roster draw it through the 4th, and `no_overlap` lets\n the next stay start on the 5th. Without it a span of days holds its last day; a span of\n moments always ends as the next may begin.\n- **`starts`** is where an add\'s field begins, stated only where that is certain \u2014 a\n log\'s own day at `today`, a request\'s requester at `me`. Nothing else starts a field\n but its declared `default` and what the reader narrowed the register to.\n\n## Record places, pins and cadence\n\nThe rarer keys of `records`, stated beside the ones on `model/records`: a title read off a photo (`scan`), the rows a child owes each day (`cadence`), where a row stands on a map (`place`, `on_site`) or on a picture (`pin`, `plan_place`).\n\n```jsonc\n// table "site": it owes a diary page each day it works, from breaking ground through handover\n"title": "name",\n"records": { "cadence": [{ "entity": "diary_page", "on": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "started", "until": "handed_over" }] }\n```\n\n<!-- generated:start records-places -->\n\n#### Record places\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `cadence` | list of [Cadence](#cadence) (at least one) | no | The rows of a child each row owes one of a day, on the days of the week listed (a site\'s diary page, Monday to Saturday) \u2014 one entry a child, never owed on a day still to come. A roster of these rows over days marks a past day owed with none missing in the danger tone, counted in the legend; today\'s is due, offered to add and never marked; each row\'s total reads the days it holds of those owed |\n| `scan` | `"text"` \\| `"iso6346"` \\| `"code"` | no | Its title, a text field, is painted or printed on the thing a row is (a container\'s number, a seal\'s) and read off one photo on a phone, beside each search matching it and each field holding or picking it: `iso6346` a container number, refused unless its check digit holds; `text` as it stands; `code` a barcode or QR code on the thing, decoded on the device from the photo with no agent |\n| `place` | [Place](#place) | no | Where a row stands, as plain fields of it, drawn as one place where its record draws the address: how sure it is, a press opening it in the device\'s maps, and one taking it from where the device stands |\n| `plan_place` | [Plan place](#plan-place) | no | Each row is a place on the plan a `plan` register draws: its title the place\'s code, its subtitle a note. A flat list, no tree |\n| `on_site` | [On site](#on-site) | no | Photos taken where a place stands (a site diary\'s): a photo taken through a phone\'s camera here keeps where the device stood, marked at the place, away from it or unknown, and a record holding one taken away says so among its checks \u2014 one with no place (picked on a computer, or kept before places were) is marked and warns nothing; a place reaches only members of the organization, through the apps drawing these photos |\n| `pin` | [Pin](#pin) | no | Each row is a place on a picture (a snag on a flat\'s plan, a spot on a container\'s drawing): a List of these rows draws the picture its `on` row holds with a numbered pin at each row\'s own place, each row led by its number, the row opened lit on it and a pin pressed opening its row; where the List writes rows, a press on the bare picture adds one there, and a pin dragged moves its row. A List of rows that link to these by a single link draws their places the same way |\n| `size` | [Record size](#record-size) | no | The real size of the thing a row is (a carton, a piece of furniture, a pallet): three number fields stating one length unit alike, read as one fact \u2014 "400 \xD7 300 \xD7 250 mm", a part the row lacks "\u2014" \u2014 after the subtitle wherever the row\'s line and head draw facts, never also as the subtitle\'s fields. A plan register drawing these rows draws each box at this size |\n\n#### Cadence\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | text | yes | The table owed: each of its rows stands on one row of this table by its single link to it, and on the first date of its line (`records` title or subtitle) |\n| `on` | list of `"mon"` \\| `"tue"` \\| `"wed"` \\| `"thu"` \\| `"fri"` \\| `"sat"` \\| `"sun"` (at least one) | yes | The days of the week one is owed on, each listed \u2014 `mon` to `sun` |\n| `from` | alias | no | A date of this row: the first day one is owed (a site breaking ground, a contract\'s start); absent, no first day |\n| `until` | alias | no | A date of this row: the last day one is owed (a site\'s handover, a contract\'s end); empty or absent, through today |\n\n#### Place\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `address` | alias | yes | A text field: the address, as people write it |\n| `latitude` | alias | yes | A number field: degrees north, negative south |\n| `longitude` | alias | yes | A number field: degrees east, negative west |\n| `accuracy` | alias | no | A number field: how many metres off the device that gave the place said it may be |\n\n#### On site\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photos` | list of alias (at least one) | yes | Files fields of this row holding photos taken on site |\n| `place` | alias | no | A single link of this row to the row whose `place` its photos are taken at (a diary page\'s site); absent, this row\'s own `place` |\n| `radius` | number | yes | How many metres from the place a photo still counts as taken there |\n\n#### Pin\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `on` | alias | yes | A single link of this row to the row whose picture (its `records` `image`) the place is on \u2014 a container\'s drawing, a floor\'s plan |\n| `x` | alias | yes | A number field of this row: how far across that picture the place stands, in percent from its left edge |\n| `y` | alias | yes | A number field of this row: how far down that picture the place stands, in percent from its top edge |\n\n#### Record size\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `across` | alias | yes | A number field: the width of the thing a row is, across |\n| `along` | alias | yes | A number field: its depth, along |\n| `up` | alias | yes | A number field: its height, up |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.scan` | A title read off a photo or a code (`scan`) is a text field the row stores. |\n| `records.cadence` | A `cadence` names each child once \u2014 its rows standing on one row of this entity by their single link and on the first date of their line \u2014 lists each day of the week once, and `from` and `until` are two different days of this entity. |\n| `records.place` | A `place` names four different fields of the row: its address a text field, its latitude, longitude and accuracy number fields it stores. |\n| `records.plan_place` | A `plan_place` names `kind` and `reach_from` as single selects, `sheet` as a single select or a one-row link, `stacked` as a boolean and the rest as numbers, each a different field; `kind`, `x`, `y` required. `size` names three numbers (`across`, `along`, `up`) stating one cell in one length `unit` alike. A `sheet` linking to an entity that states an `image` draws that drawing under the places, `scale` (a number field of that entity) its width in cells; `scale` goes with such a `sheet` alone. |\n| `records.on_site` | `on_site` lists files fields of the row, each once, taken at the `place` of the row itself or of the one row a single link of it names (`place`), which states one. |\n| `records.pin` | A `pin` names a single link of the row (`on`) to an entity whose `records` states its `image` (a PDF is drawn as its first page), and two different number fields of the row, how far across (`x`) and down (`y`) that picture in percent. A List of rows whose own entity states a `pin` is drawn over that picture; `on`, `x` and `y` are set by the press that adds a row, never asked. |\n| `records.size` | A `size` names three different number fields of the row (`across`, `along`, `up`) stating one length unit alike (mm, cm, m or km); they read as one fact after the subtitle, so the subtitle never also names one. |\n\n<!-- generated:end records-places -->\n\n## Status\n\nA record\'s `status` is the stage a row moves through \u2014 a stored single select, each\noption\'s moment the date `dates` names \u2014 drawn as a badge beside its title wherever the row is.\n\n<!-- generated:start status -->\n\n#### Status\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The single select that is the row\'s state, its options in the order the work goes through them |\n| `closed` | list of alias (at least one) | no | Options that end the work: a register opens on the rows not at them, and the badge reads muted |\n| `dropped` | list of alias (at least one) | no | Options of `closed` that leave the work undone (cancelled, rejected): whoever holds the row moves it to one from any stage, where every other move forward passes each owned stage between that binds the row (its `when`) and needs each one\'s owner, and a row leaving one enters a stage as a row made there does |\n| `history` | text | no | A table every write that sets or moves the status appends one row to (its link to this table, the new status, the moment and who) \u2014 by the record write path, whoever writes: for reporting on every move |\n| `dates` | map of alias \u2192 alias | no | Option alias \u2192 a date field of this entity the record write path stamps with the moment a row moves into that option, or is made at it \u2014 whoever writes. A value the same write states for that field wins, so past data loads as it happened; the platform never clears a stamp, so each is when the row last entered the option. A stage\'s date is made where none is named |\n| `stages` | map of alias \u2192 [Stage](#stage) | no | Option alias \u2192 who owns that stage and how long it should take: the owner alone takes a row out of it, by any write, and presses the acts offered there; every move forward past it of a row it binds (its `when`), and every such row made past it, is its owner\'s too. Its days run from the moment its `dates` field holds. In an app the status then moves only by acts that `set` one of its options \u2014 no save writes it and no act asks it |\n\n#### Stage\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `owner` | text \\| [Stage group](#stage-group) | yes | Who owns the stage: a member field of the row, `link.field` read through a one-row link (not at the stage a row opens at, whose owner made the row: a member field of the row itself), or `{ group }` |\n| `within` | integer | no | Whole days the stage should take from the moment the row entered it |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| [comparison](#values) \\| `{ "empty": true \\| false }`) | no | The rows that must pass this stage: a row meeting every entry on its own fields, as it stood before a write or as the write leaves it \u2014 a single select holding one of these options, a yes/no this value, a number or date meeting its bounds, a field empty or filled. Absent, every row must. A row at the stage leaves it by its owner either way |\n\n#### Stage group\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `group` | text | yes | A member group\'s id (from Settings, or chat): every member of it owns the stage |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.status` | A status is one single select, its options in the order the work goes through them, `closed` naming the ones that end it and `dropped` those of them reached from any stage. |\n| `records.history` | A declared status `history` has exactly one link to the row it records, one single select holding every option of the status (or a text), exactly one date and at most one member. |\n| `records.stages` | A stage\'s owner (`status.stages`) is a single member field of the row, one read through a one-row link (never at the stage a row opens at: its owner made the row, so it is a member field of the row itself), or a group, on an option that does not end the work; a group\'s stage needs the status `history` with who moved the row. Every write takes a row off a stage as its owner alone, and moves a row past it or makes a row past it as its owner alone where the stage binds the row (its `when`) \u2014 a move to a `dropped` option excepted, which whoever holds the row makes from any stage; a row leaving a dropped option, or holding none, enters a stage as one made there. Each stage\'s moment, and each open option\'s, is the date its `dates` names, made where none is named, which every write that opens or moves the status stamps; `within` runs from it. In an App a status with stages moves only by Acts that `set` one of its options \u2014 no save writes it or a date it stamps, and no Act asks either, sets it otherwise or writes either from a workflow; an Act moving a row forward past a stage another owner owns, into an option not `dropped`, keeps its `when` off every row that stage binds. |\n| `records.stage-when` | A stage\'s `when` names the rows that must pass it, as an Act\'s `when` holds them \u2014 single selects to their options, yes/nos to `true` or `false`, numbers and dates to bounds, any field to `{ "empty": true \\| false }` \u2014 on fields a write stores of the row itself: never the status, a formula, a lookup or a rollup, and never on the stage a row opens at. It binds a row meeting it as it stood before a write or as the write leaves it; absent, it binds every row. |\n| `records.detour-note` | *Noted, never refused:* A stage the work only steps aside into \u2014 not where a row starts, entered from one step alone that also moves the work on without it, and left by its own moves only back to that step (a pause, a repair) \u2014 is a detour: its option and the Section whose `at` holds it stand before that step\'s, else the work reads it as its next step, and the Act moving the work into it leads that Section \u2014 never a correction waiting in a \u22EF. |\n\n<!-- generated:end status -->\n\n- **The status** is one single select. `closed` options end the work: a register opens\n on the rows not in them. A `history` table gets one row per move \u2014 its link to this\n table, the new status (a select holding the same option aliases, or text), the\n moment and who \u2014 appended by the record write path in every write that opens a row at a\n stage or moves it, whoever writes (the member behind the write is its `by`; an automation\n leaves it empty). A `history` the tables do not declare is derived, and needs no\n `records` of its own.\n- **When each step happened** is the date `dates` names for its option, stamped as the row\n enters it, whoever moves it. One way times a status: a straight-line process is a select\n whose options are its steps in order, each with its date \u2014 never a run of dates the stage\n is read off. An Act that moves the stage and `asks` the step\'s date with `starts: "today"`\n lets the reader record a day already past in the same press.\n\n## Write rules\n\nWhat a WRITE of a table\'s rows meets is stated in the table\'s document, where it applies: `min` and\n`max` on a number or date field, `options_where` and `same` on a link, `suggest` on a text field,\nand `no_overlap` and `natural_key` on the table. A key a field\'s type does not take is refused at\nits path, and a document has no `write_rules` key.\nEvery one but `natural_key` and `suggest` holds on every path a record is written by \u2014 the grid, a record\'s form, an import, a workflow, an\nagent, the API, an app \u2014 judged in the write\'s own transaction, every broken rule listed at once; the\nscreen and a picker only mirror them. A row that already breaks one stays editable until a write\ntouches what the rule reads. `natural_key` names the fields an app recognises a row by, to reuse it where an\nadd or a find-or-create meets it; a record tool or the API creates the row it is handed. `suggest` offers\nvalues in a text control and refuses none. An `options_where` link is refused where a write names a row the\nnarrowing does not admit, and every picker of the link offers only the rows it admits.\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "label": "Customer",\n "fields": { "email": { "type": "text", "label": "Email", "unique": true } },\n "natural_key": ["email"]\n }\n ]\n}\n```\n\nA customer above is RECOGNISED by their address. An add that names one \u2014 from the customers\nregister or from a picker\'s "new" \u2014 reuses the row it matches and opens one only where nothing does,\nso the book never grows a second Acme.\n\n```json\n{\n "tables": [\n { "ref": "product", "label": "Product", "fields": { "name": { "type": "text", "label": "Name" }, "in_stock": { "type": "number", "label": "In stock" } } },\n {\n "ref": "order",\n "label": "Order",\n "fields": {\n "name": { "type": "text", "label": "Name" },\n "placed_on": { "type": "date", "label": "Placed on" },\n "due_on": { "type": "date", "label": "Due on" },\n "lines": { "type": "link", "label": "Lines", "table": "order_line", "multi": true, "pair": "order" }\n }\n },\n {\n "ref": "order_line",\n "label": "Order line",\n "fields": {\n "name": { "type": "text", "label": "Name" },\n "order": { "type": "link", "label": "Order", "table": "order", "multi": false, "pair": "lines" },\n "quantity": { "type": "number", "label": "Quantity", "min": 1, "max": 9999 },\n "ships_on": { "type": "date", "label": "Ships on", "min": "order.placed_on", "max": "order.due_on" },\n "product": {\n "type": "link",\n "label": "Product",\n "table": "product",\n "multi": false,\n "options_where": {\n "node_type": "group",\n "logic": "and",\n "children": [{ "node_type": "condition", "type": "number", "field_key": "in_stock", "operator": "greater_than", "value": 0 }]\n }\n }\n }\n }\n ]\n}\n```\n\nA line of nothing is not a line: `quantity` is refused on create and on update. A line ships inside\nits order\'s window \u2014 a bound read off the row a link names is judged when the line is written. Nothing is sold off an empty shelf: every picker of `product`\nreads only the rows that answer its `options_where`, and every save refuses the same rows again.\n\n```json\n{\n "tables": [\n {\n "ref": "booking",\n "label": "Booking",\n "fields": {\n "room": { "type": "text", "label": "Room" },\n "state": { "type": "select", "label": "State", "options": [{ "alias": "held", "label": "Held", "color": "blue" }, { "alias": "released", "label": "Released", "color": "gray" }] },\n "starts_on": { "type": "date", "label": "Starts on" },\n "ends_on": { "type": "date", "label": "Ends on", "min": "starts_on" }\n },\n "no_overlap": { "from": "starts_on", "to": "ends_on", "by": ["room"], "while": { "state": ["held"] } }\n }\n ]\n}\n```\n\nOne booking per room at a time, while it is held \u2014 a create or a save whose span overlaps another\nheld booking of the same room is refused.\n\nA table\'s `checks` (the conditions no write of its rows may meet) and `workflows` (what follows every committed write of them) are stated in its document and hold on every path a record is written by; their keys, what they read and an example are on `workflows/table-checks` and `workflows/table-workflows`.\n\n<!-- generated:start write-rules -->\n\n#### Suggest\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `table` | text | yes | The table whose text field is offered from: its id, or its `ref` in this call |\n| `field` | alias | yes | The alias of that table\'s stored text field; the field\'s control offers, as the reader types, the distinct values it holds, and any text is still written |\n\n#### No overlap\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | text | yes | The stored date (or datetime) a row\'s span starts on: `<field alias>` of the row, or `<link alias>.<field alias>` of the one row a one-row link names (a booking line\'s piece held for its booking\'s dates) |\n| `to` | text | yes | The stored date (or datetime) a row\'s span ends on, named like `from` and through the same link \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at |\n| `by` | list of alias (at least one) | no | Fields two rows share to compete for a span (the same employee, the same room); absent, every row |\n| `while` | map of text \u2192 list of alias (at least one) | no | Only rows at these options of these selects hold their span (running, not closed), the select named like `from` \u2014 `<field alias>` or `<link alias>.<field alias>`; absent, every row |\n\n<!-- generated:end write-rules -->\n\n<!-- generated:start rules-write -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `write.natural-key` | A `natural_key` is text or a number a person types back, or a one-row link; a text key is unique \u2014 `unique: true`, or the key\'s fields as a set in the table\'s `unique`. |\n| `write.bounds` | `min` and `max` bound a number or a date \u2014 by a constant a number, by a field of this row or of the one row a one-row link names either, of the same type and unit, never by itself \u2014 and `min` is never above `max`. |\n| `write.bound-per-row-unit` | On a figure whose unit or currency is the row\'s own (its notation\'s `per_row`) a constant bound is only 0 \u2014 bound it by a field of the row. |\n| `write.suggest` | `suggest` stands on a stored text field and names a `table` and a `field`, a stored text field of that table; the app reads that table, so it has a `records` entry. |\n| `write.same` | `same` stands on a link and names one-row links this table and the link\'s target both hold to one table. |\n| `write.options-where` | `options_where` narrows a link by an `and` group of plain conditions over the target\'s own fields \u2014 a number compared in one unit on every row, a select by options it declares. |\n| `write.no-overlap` | `no_overlap` spans two date fields, the row\'s own or the one row a one-row link names (`booking.start_on`); `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options, the row\'s own or that linked row\'s. |\n\n<!-- generated:end rules-write -->\n\n- A `natural_key` is `text` or `number`, because a person types it back, or a\n one-row link, because a row is also recognised by the one row it belongs with (a\n fee by its visit and its kind). A key holding a `text` field is unique: `unique:\n true` on that field, or the key\'s fields as a set in the table\'s `unique` (a\n site by its customer and its name) \u2014 two rows sharing it would make\n find-or-create pick whichever the read answered first. An add asks the key by\n default.\n- A `same` link names only rows that name what this row names \u2014 a fee\'s invoice\n is one of its own visit\'s \u2014 through a one-row link both tables hold to one\n table; its picker offers nothing until the row names one. A row added under a\n record names what it copies off it (`default_from`) from the start, so a block\n expecting one line per row of the link lists that record\'s rows alone. A save\n of this row moving what the two compare is refused while they would name apart.\n- An `options_where` link may be left empty; a row it names is refused on every\n save that writes the link unless it holds the narrowing, and the refusal names\n the link and the row. A save that leaves the link alone is not judged, so a row\n still naming a row the narrowing no longer admits stays editable. A link naming\n one kind of a book\'s rows (a "Shipping line" among the parties) narrows to that\n kind, or its picker offers every row.\n\nA field\'s own `required` is not here: the field carries it, and every write\npath refuses the row by field name from it. `unique` is the text field\'s own\nclause (\xA7 `text`) \u2014 an add says so at the control before the column does. A\nfield the workspace writes (a formula, a rollup, a lookup, an autonumber, a date\nwith `derive_from`) is never asked, written or edited.\n\n## App tree\n\nAn app is a tree of components, assembled the way a page is written in JSX: pick a component, bind it to a table by\nname, and it draws well with no props, because its defaults come from the table\'s facts (\xA7 Records, \xA7 Status,\n\xA7 Write rules, \xA7 Field). A prop overrides a default at its own node. No prop says how anything looks: the runtime\ndraws each concept one way, and a screen the job needs that no component states is told to the person \u2014 the job,\nwhat the app drew, the word wanted \u2014 never props bent to approximate it.\n\nEach component is a page of its own: `model/app-node`, `model/list-node`, `model/record-node`, `model/section-node`,\n`model/side-node`, `model/page-node`, `model/outside-node`, `model/tab-node`, `model/field-node`, `model/act-node`, `model/check-node`,\n`model/reading-node`, `model/progress-node`, `model/statement-node`, `model/export-node`, `model/book-node`,\n`model/query-node` and `model/workflow-node`. Beside them: `model/changing-an-app` and `model/what-an-app-may-write`, `model/layouts`, `model/time-and-place-layouts` and\n`model/publish-desk` for a List\'s `layout`, `model/composed-page`, `model/tasks`, and the rules, `model/app-rules`,\n`model/reading-rules`, `model/statement-rules`, `model/check-rules`, `model/register-rules`, `model/block-rules`, `model/record-rules` and `model/act-rules`.\n\n<!-- generated:start app-components -->\n\n| Component | Draws | Holds |\n|---|---|---|\n| [App](#app-node) | the launcher tile, the app\'s words, its Lists, and its API | List, Tab, Reading, Query, Workflow |\n| [List](#list-node) | rows of an entity: the register, a record\'s child rows, a roster\'s cells, related rows, a log | Tab, Reading, Progress, Export, Act, Check, Book, Record, List |\n| [Record](#record-node) | what a row opens: the head and the parts | Field, Section, Side, Page, Outside, List, Reading, Progress, Statement, Act |\n| [Section](#section-node) | a titled part of a record, and optionally a step of its path | Field, List, Reading, Progress, Statement, Act |\n| [Side](#side-node) | the part the rest is worked from, beside them | Field, List, Reading, Progress, Statement, Act |\n| [Page](#page-node) | a part on its own page, off the record\'s scroll | Field, List, Reading, Progress, Statement, Act |\n| [Outside](#outside-node) | what one record shows, and which acts it allows, to a person with no seat who opens a link a member made of it | Field, List, Act |\n| [Tab](#tab-node) | one view of a List\'s rows over a period, or one sheet of a dashboard | Reading, List |\n| [Field](#field-node) | one field: a control, a gallery, prose, a link and its picker, the rows filed under a back-link | \u2014 |\n| [Act](#act-node) | a verb: what pressing it does | \u2014 |\n| [Check](#check-node) | a formula of the rows that warns, or refuses the writes and acts it names | \u2014 |\n| [Reading](#reading-node) | a number over rows: one figure, its parts, a grid, per period, steps; or a record\'s own figure | \u2014 |\n| [Progress](#progress-node) | the work against its plan, day by day | \u2014 |\n| [Statement](#statement-node) | a figure built from figures of the record | \u2014 |\n| [Export](#export-node) | a report file of the rows in view | \u2014 |\n| [Book](#book-node) | another table read as part of the register | Record, Act, Check |\n| [Query](#query-node) | nothing: a read for agents | \u2014 |\n| [Workflow](#workflow-node) | nothing: a verb for agents | \u2014 |\n\n<!-- generated:end app-components -->\n\n### A node\n\nA node is `{ "type": <Component>, "key"?, ...props, "children"?: [...] }`. `type` names the component, `key` its\nname among its siblings, the other keys its props, `children` the nodes it holds. An unknown prop is refused.\n\n- **Two shorthands, both for Field.** A bare string child is a Field (`"due_date"`); an object with `field` and no\n `type` is a Field with props (`{ "field": "price", "edit": false }`). Both work in every prop that lists fields:\n `create`, `row.subtitle`, `add`, and the `children` of a Record or a part. A column is a field\'s alias, or\n `{ "field", "edit": true }` for the line\'s own control, and takes no other prop.\n- **Fields and options are named by alias**, never by label. A tagged value is an object, never a magic string.\n- **`false` turns a default off at its node:** `"figure": false` drops the row\'s figure, `"edit": false` makes a\n field read-only, `"create": false` and `"remove": false` take a List\'s add and Delete away, `"readings": false` its\n band, `"row": { "status": { "edit": false } }` the status buttons (another app moves the stage) and\n `"row": { "image": false }` the picture.\n\n### Keys and addresses\n\nA key is unique among its siblings, the way a React key is. A node that binds data takes its key from what it binds:\n\n| Node | Key when none is stated |\n|---|---|\n| Field, Check | its field alias |\n| List, Book | its entity alias |\n| Record | `record` |\n\nEvery other node states its key. Three kinds of key are unique across the whole app, because each is an API name:\nan Act\'s (its workflow is `act_<key>`, or the one it names), a Reading\'s (its query name for `run_app_query`), and a Query\'s or\nWorkflow\'s.\n\nA node\'s **address** is the path of keys from the app, such as `customer/record/orders`; the app itself is `/` (the `in` of a node at the root), and its own key is part of no address. A patch may use any ending\nof that path that names exactly one node (`orders`). The exact path wins where one exists; an ending naming two\nnodes is refused, and the refusal lists both paths. `get_app` prints the tree with each node\'s address.\n\n### Values\n\n| Value | Written |\n|---|---|\n| now, the reader, an asked input | `{ "now": true }`, `{ "me": true }`, `{ "input": "name" }` |\n| a computed value | `{ "formula": "{cost} * 1.2" }` over the acted row\'s fields |\n| a call\'s own value, in a Query | `{ "param": "name" }` |\n| cleared | `null` in an Act\'s `set` |\n| an option | its alias |\n\nA **condition** (`when`) maps a select to a list of its option aliases and a yes/no to `true` or `false`; every entry\nholds. A number (a number, currency or percent field, or a formula or rollup yielding one) takes a comparison with any\nof `gt`, `gte`, `lt`, `lte` (at least one, never both `gt` and `gte` or both `lt` and `lte`), each a number; a date\ntakes the same keys with a day (`"2026-03-01"`), a moment on a datetime (`"2026-03-01T09:30"`) or `{ "now": true }`\n\u2014 today, or the current minute on a datetime, read when the rows are, so overdue is `{ "due_on": { "lt": { "now": true } } }`;\nand any field but a yes/no takes `{ "empty": true }` or `{ "empty": false }`. A comparison on a field it does not fit is\nrefused, naming the field, its type and what it takes. A `when` on an Act, a Section or other block, and a List\'s `where`\n(the register\'s or a child List\'s) take these; a child List that adds, picks or expects rows refuses a comparison in its\n`where`, since a row it holds could stand outside it. A **narrowing** (`opens`, a Reading\'s, a Tab\'s or an Export\'s `where`) takes options, yes/nos and also `{ "me": true }` on a member\nfield, the reader; `where` on the register, a log or a record\'s child rows fixes the rows on the server for every reader, so it refuses\n`{ "me": true }` \u2014 open on it with `opens`; on a record\'s related rows (`same`, `app`) and on a dashboard\'s List it\nnarrows by the reader and takes it, and those two take no comparison.\n\n## Changing an app\n\n1. `get_workspace` with `kind: "app"` names the app. `get_app` prints the `tree:`, the `node addresses:` (the address of\n each node the app draws, `(default)` on those the tree leaves to the platform) and the `version` line, whose id is the `base_version_id` to send. A member who is not the\n app\'s author is shown no tree. Keep the tree: no tool reads an earlier one.\n2. `set_app` with `app_id`, `base_version_id` (that version: a newer write is refused, not overwritten) and `nodes`,\n (a refused change writes nothing, so `dry_run` is for reading the permissions it would grant and remove). Every answer\n states the permissions the change grants and removes.\n3. `screenshot_app` shows the result.\n\n`nodes` maps an address to a patch of that node. Read against the customers List of the account review (\xA7 A complete\nworkspace, abridged), the call below makes `tier` read-only, moves the Orders Section ahead of Account, adds an Act to\nAccount and removes the Reading:\n\n```json\n{\n "type": "List",\n "entity": "customer",\n "children": [\n { "type": "Reading", "key": "ordered_by_tier", "by": "tier", "value": "total_ordered" },\n {\n "type": "Record",\n "children": [\n { "type": "Section", "key": "account", "title": "Account", "children": ["tier"] },\n { "type": "Section", "key": "orders", "title": "Orders", "children": [{ "type": "List", "entity": "order" }] }\n ]\n }\n ]\n}\n```\n\n```json\n{\n "app_id": "<id of the app>",\n "base_version_id": "<current_version_id>",\n "nodes": {\n "tier": { "edit": false },\n "orders": { "before": "account" },\n "mark_gold": { "type": "Act", "in": "account", "label": "Make gold", "when": { "tier": ["standard"] }, "set": { "tier": "gold" } },\n "ordered_by_tier": null\n }\n}\n```\n\nEach entry applies in order:\n\n- **An existing node:** a JSON merge patch (RFC 7396) of its props; `null` drops a prop, and a list (`children`,\n `columns`) replaces the node\'s whole.\n- **`null`:** removes the node and everything under it.\n- **An address naming no node, with `type` and `in`:** adds a node there; its key is the address\'s last word, and a\n Field states `field` in place of `type`.\n- **`in`, `before`, `after`:** move a node into another, or ahead of or behind a sibling.\n\nThe app\'s own props (`name`, `description`, `icon`, `color`, `words`, `scope`, `period`) change\nthrough `body`, a merge patch over the App node. `dry_run` checks a change and writes nothing.\n\n| To change | State |\n|---|---|\n| a field read-only | `"edit": false` on its Field, or on the Section, Side, Page or Record that draws it |\n| a column added or dropped | `columns` of the List, every column kept: a List that never stated `columns` draws none beyond the row\'s title and its `records` line |\n| a button | an Act node (\xA7 Act node); `confirm` makes its press ask first |\n| a label | the table\'s field `label` in `set_tables`, for every app; for this app alone, `body: { "words": { "<table>": { "fields": { "fee": "Consultation" }, "options": { "urgency": { "rush": "Same day" } } } } }`; a List\'s `label`, a Section\'s `title`, an Act\'s `label`, `add_label` on the add. A Field\'s `label` heads only a gallery or prose block |\n| the order of nodes and rows | `before` or `after` a sibling, or `in` another parent, in the node\'s entry (a Section ahead of another, a Field ahead of another in its Section); a List\'s `columns` read in the order stated; its rows read in its `sort`: `{ "sort": { "field": "collected_on", "desc": true } }` |\n| the row\'s second line | `row: { "subtitle": ["mechanic"] }` on the List: up to two fields under the title, in place of the table\'s `records` line, which stays as it is |\n| a picker\'s add | `add` on the link Field: `{ "add": ["species", "microchip"] }` asks those fields of the new row; `false` makes the picker only pick. It holds in every picker of that table in the app |\n| a linked row fixed or deleted where its link stands | `record` and `remove` on the link Field, or a row `title` that is a link: `{ "add": ["name", "kind"], "record": ["name", "kind"], "remove": true }` \u2014 the link\'s card opens the row in the drawer, saving those fields, with its Delete. Stated once per table, it holds on every card of that table in the app; a table a List of the app draws is worked in that List, and the statement there is noted and grants nothing |\n| a headline figure | a Reading in the List: `{ "type": "Reading", "in": "orders", "key": "prepaid_owed", "label": "Prepaid owed", "value": "prepaid", "where": { "stage": ["awaiting_payment"] } }` sums the rows in view; without a `value` it counts them |\n| a calendar and its date | `layout: "calendar"` on the List, and the date it places each row on is the first of `row.due`: `{ "layout": "calendar", "row": { "due": ["pickup_on"] } }`; absent, the table\'s `records.due` |\n| a record\'s child lines | a List inside the Record\'s Section over the child table: `{ "type": "List", "in": "details", "entity": "repair_line" }`; the child table (one link back to the record) and the record\'s rollup over its rows come from `set_tables` first. Lines priced off a book: `docs path="design/treatments/moves-and-numbers/priced-lines"` |\n\n**A node the tree leaves to the platform has an address too.** `get_app` lists the nodes the app draws, those the stored\ntree does not state marked `(default)`: the Record with its Sections and Fields, the status buttons, the band of readings.\nPatch one by its address and the call states it first: the nearest node the tree does state gains the default child that\nleads to it, as drawn (the whole Record under a List that states none, with the List\'s `row` picture and the `columns` the\ndefaults added; every Section of a Record that states no part; every status button of the app, each in the Section of its\nstage; every reading of the band, since stating one stands the others down), then the patch applies. Defaults\noutside that subtree stay defaults, and the answer\'s notes say what was stated. Removing every reading, or every status\nbutton, is refused: a set no node states is drawn again by the platform, and only `readings: false` or\n`row.status: { "edit": false }` on the List draws none. **A stated subtree stays as it is**: a\nfield or option the tables gain later is not added to it, so name it in the tree. `{ "due_date": { "edit": false } }`\nover a sparse tree makes that field read-only and takes away `saves <table>: due_date` alone. An address that ends several\ndrawn nodes is refused, as for stated ones.\n\n## What an app may write\n\nAn app states no list of what it writes: the tree derives it, and the generated workflows accept exactly that, so it is\nalso what an agent can do through the app\'s API. `get_app` prints it beside the tree.\n\n1. **A save of field `f` of table `E`** exists when a node draws `f` on a page of `E`\'s rows (a Field in a Record,\n Section, Side or Page, or a part of the head), the table lets a person write it (stored; not computed, stamped or the\n back side of a child\'s link), no Act of the app on `E`\'s rows writes it, and neither the node nor a container above it\n says `edit: false`. A row\'s stage is moved by an Act or the one button per move a status no Act moves gets, by hand only where `row.status` says `{ "edit": true }`, and by none where it says `{ "edit": false }`.\n2. **An add of `E`** exists for a List over `E` that writes its rows unless it says `create: false` (asking what it lists, and\n setting the link back to the record itself), and for an `add` on a Field drawing a picker of `E`. An absent `create`\n stands down where the table states no `singular` (nor the app\'s `words`), or where `where` keeps a select at an option an\n Act sets other than the one a new row starts at; a stated `create` list over such a table is refused. A roster\'s cells and\n a document are added only where `create: true`.\n3. **A Delete of `E`** exists for a List over `E` unless it says `remove: false`, and for the table of every Book of that\n List (a Book has no `remove` of its own). A roster\'s cells, a document and a desk\'s rows are deleted only where `remove: true`.\n `remove: false` takes this app\'s Delete away and no more: the record tools, the API and other apps still delete. A row no\n surface may delete is a table check with `on: ["delete"]`.\n4. **An act** exists for each Act node. Every field it sets, asks, files into, signs or records is the act\'s, and read-only\n everywhere else in the app.\n5. **The table\'s contract holds on every path** and is not restated here: required, unique, no overlap, field rules, table\n checks, stage owners, history and the read scope. (`natural_key` is read by an app\'s add and find-or-create alone.)\n\nDrawing is reading or saving; a List that writes its rows also adds and deletes them, until it says `create: false` or `remove: false`, and a List below a part or a Record that says `edit: false` writes, adds and deletes none.\n\n### What a table gives a component\n\n| Table fact | Feeds |\n|---|---|\n| The title | every row\'s name: Lists, a Record\'s head, link chips, pickers, an add |\n| The row\'s line (`records` subtitle, figure, image, mark, party, description) | the row\'s line and head, cards, a checklist\'s photo |\n| The stage (`records` status) | the stage chips, a List\'s `opens`, the badge, an Act\'s moves, stage owners gating acts, a Section\'s `at` |\n| Deadlines (`records` due, settled, frees) | countdowns and overdue marks, the default sort, attention order, spans on lanes, calendars and rosters |\n| Bounds (`records` limits, gates, tolerance) | the figure\'s meter, wherever it stands |\n| Tasks and cadence (`records` task, cadence) | the ring on task rows, a roster\'s owed days |\n| Applicability and starts (`records` applies, starts) | a field shown and asked only where it applies; an add\'s starting values |\n| A field\'s `required`, `default`, `default_from`, `natural_key`, `unique` | what an add asks, and what it starts at |\n| A field\'s `notation`, `format`, options, `options_where` | money, units, chips, and what a picker offers |\n| The table\'s `read_scope` | which rows each reader reads, in every List |\n\n### Where the rules stand\n\n\xA7 App rules and the rule pages after it are every rule the check holds over an app, by id, whichever component it concerns. A\n**language** is decided by the app\'s own words: a tree names no locale, so what an app draws itself (its buttons, empty\nstates, refusals) is in the language its labels are written in, and `words` count as labels. Mix two languages in one\napp and it answers in two.\n\n## App node\n\nAn app draws its launcher tile, its words, its Lists and its API. It binds the tables it names: `set_app` fills\n`tables` with the id of every table alias the tree uses, and `templates` with that of every document template alias.\nA relabel on either side is a rename and no tree points at another table by accident.\n\n<!-- generated:start node-app -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The app\'s alias, its name among apps; the App node is addressed `/`, and no address in it starts with this key |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A Lucide icon name in kebab-case the launcher tile draws |\n| `color` | [colour](#select) | no | The launcher tile\'s colour |\n| `words` | map of alias \u2192 [Table labels](#table-labels) | no | A table \u2192 the words this app draws for it |\n| `scope` | alias | no | A required one-row link of the first List\'s entity: the app works inside one row of the linked entity |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `uses` | list of alias (at least one) | no | Tables the app\'s Query and Workflow nodes read without drawing them, by alias: they join the tables the app reaches, so a workflow may name them |\n| `period` | `"month"` \\| `"quarter"` \\| `"year"` \\| [Dashboard period days](#dashboard-period-days) | no | The period a dashboard opens on |\n| `tables` | map of alias \u2192 text | no | The id of each table the tree names, by its alias: filled by set_app, never written |\n| `templates` | map of alias \u2192 text | no | The id of each document template the tree names, by its alias: filled by set_app, never written |\n| `children` | list of ([List](#list-node) \\| [Tab](#tab-node) \\| [Reading](#reading-node) \\| [Query](#query-node) \\| [Workflow](#workflow-node)) (at least one) | no | The first List at the root is the register, and a second is another app; a dashboard holds Tabs, Readings and Lists that open in another app. Queries and Workflows stand here too |\n\n#### Table labels\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | no | The table\'s name in the plural (a register\'s heading, a block\'s, a link\'s picker) |\n| `singular` | text | no | One row of the table, as the add and empty sentences name it |\n| `fields` | map of alias \u2192 text | no | A field alias \u2192 the words this app draws wherever it names the field; the table\'s field keeps its own label |\n| `options` | map of alias \u2192 map of alias \u2192 text | no | A select field alias \u2192 option alias \u2192 its words |\n\n#### Dashboard period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | `30` | yes | The last 30 days through today \u2014 the one span of days a dashboard opens on |\n\n<!-- generated:end node-app -->\n\n- **The first List is the register**: the working list of one table. Every further List at the root is a **log** beside\n it, at most 3, each stating its `label`: read as what was brought into or done to the register\'s rows (the files\n imported, the runs of a batch), opened as a register of its own inside the app, its reads, acts and workflows the\n app\'s. Only the organization\'s members are shown a log. A log is a table or cards with no `Tab`, `Book` or `compose`;\n its table is never the register\'s, a Book\'s or another log\'s; none beside `scope`. A list people work every day as\n a desk of their own is another app.\n- **A dashboard** is an App with no register: it holds `Tab`s, `Reading`s and read-only `List`s that open in\n another app (`app`). One `period` windows every reading placed in time: this month by default, or `quarter`, `year`\n or `{ "days": 30 }`. `words` and `scope` belong to a register.\n- **Query and Workflow** nodes stand at the root and are never drawn.\n- **Writes:** nothing itself.\n\n## List node\n\nA List draws rows of one table. What its rows are depends on where it stands:\n\n- at the App root: the register (the first) or a log (each further one);\n- under a Record, Section, Side or Page: the record\'s child rows, filed through the child\'s one link to the record\n (`via` where it has several);\n- under a roster List: the rows that fill the roster\'s cells;\n- with `same` or `app`, or on a dashboard: related rows, read only.\n\n<!-- generated:start node-list -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Absent, the entity alias |\n| `entity` | alias | yes | The table whose rows the List draws |\n| `via` | alias \\| text | no | The child\'s link to the record, where it has several. A List that only reads may state `<link>.<link>`: rows linked to the record only through a row of their own \u2014 their one-row link to that row, then that row\'s one-row link to the record |\n| `same` | alias | no | A one-row link of the record: lists the rows sharing that link\'s row, the record left out, read only |\n| `where` | [narrowing](#app-tree) | no | A fixed narrowing on the server, for every reader: a select holds one of these options, a yes/no this value, a number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), a field is `{ "empty": true \\| false }`, and `<link>.<field>` keeps the rows whose one-row link names a row holding a select or yes/no so (a path holds nothing on an add) \u2014 every entry ANDed. A List that only reads (`same`, `app`, a Record with `edit: false`) also bounds a date or number by a field of the record it stands beside, `{ "record": "<field>" }`, read when the rows are: a window of the record\'s own days (`{ "published_on": { "gte": { "record": "period_start" }, "lte": { "record": "period_end" } } }`); a bound the record holds blank keeps no row, and the same rows are handed to an Act\'s paper that names the List. A record bound narrows what is listed; it is never an access bound \u2014 the table\'s row rules are. An add holds a select\'s option and a yes/no\'s value; a bound or a blank holds nothing on an add |\n| `expect` | alias \\| [Expect](#expect) | no | One line per option of this select, or per row of this catalog link |\n| `under` | alias | no | A one-row link to the document that covers the line; lines stand grouped under their document |\n| `app` | text | no | The id of the register app its rows open in |\n| `row` | [List row](#list-row) | no | How this List reads its rows, where it differs from the table\'s `records` |\n| `label` | text | no | What the app\'s lists and the tab strip call the register; absent, the entity\'s label. Never the noun of its rows \u2014 the table\'s own label and singular (this app\'s `words`) are |\n| `columns` | list of (alias \\| [Edit column](#edit-column)) | no | Fields read after the row\'s title; `{ field, edit: true }` is the line\'s own control, at most 2. Absent: none, the row reads its title and its `records` line alone |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | Fields the reader narrows the rows by every day, at most 3; the stage chips always stand. A field is a select, member, link, date, yes/no or number range; `tiers` bands a number or date |\n| `search` | list of alias (at least one) | no | Fields the search box reads; absent, every word of the row |\n| `group` | alias | no | A field the rows stand grouped by; a gantt draws the groups as its lanes |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order of the rows; absent, the first `records.due` soonest, else newest |\n| `opens` | [narrowing](#app-tree) | no | The view the register opens on, a chip the reader may take away |\n| `period` | `"today"` \\| `"week"` \\| `"month"` \\| `"quarter"` \\| `"year"` \\| [Period days](#period-days) | no | The period the List opens on, with Tabs; absent, this month |\n| `attention` | `false` | no | false: rows keep the list\'s own order; absent, those needing attention open first |\n| `readings` | `false` | no | false: the register leads with no band of figures; absent, rows that move, fall due or carry money lead with one. Never with Readings stated |\n| `layout` | [layout](#layouts) | no | How the rows are drawn: a word, or an object stating one layout with its data; absent, a table |\n| `compare` | [Compare](#compare) | no | Reads each row\'s before against its after: two files fields as a pair of photos, or two values of one type as one column |\n| `current` | alias | no | A child List of revisions: the child\'s date or number field that orders them. The row holding the latest value reads as current and the others fold under "superseded \xB7 N", one press to open; the record shows the current file through a lookup of the child ordered by that field |\n| `create` | boolean \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The fields an add asks. Absent: where this List writes its rows, the table\'s required fields, its natural key and its title; `false`: rows are not added here. Where the table states no `singular` (nor the app\'s `words`), or `where` keeps a select at an option an Act sets other than the one a new row starts at, an absent `create` adds nothing; the table\'s `singular` or a stated list adds. A roster\'s cells and a document add only where `true` |\n| `add_label` | text | no | The add button\'s words; absent, the platform\'s word and the entity\'s singular |\n| `remove` | boolean | no | Rows offer a Delete. Absent: they do where this List writes its rows; `false`: this app\'s Delete is gone, while the record tools, the API and other apps still delete \u2014 a row no surface may delete is a table check with `on: ["delete"]`. A roster\'s cells, a document and a desk\'s rows delete only where `true` |\n| `pick` | `true` | no | Also picks rows of the child that no record holds yet |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `children` | list of ([Tab](#tab-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Export](#export-node) \\| [Act](#act-node) \\| [Check](#check-node) \\| [Book](#book-node) \\| [Record](#record-node) \\| [List](#list-node)) (at least one) | no | What the List holds: its Tabs, Readings, Progress, Exports, Acts and Checks, its Books, the Record a row opens, under a roster the List of its cells, and in a child List with `under` the List of the documents the lines stand under (the one place a document is added or deleted) |\n\n#### List row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | [List row title](#list-row-title) | no | The row\'s title field, stated for its props alone: `edit: false` reads it, `add` is its picker\'s add, `false` its picker only picks; where it is a link, `record` and `remove` how its card works the row it names |\n| `subtitle` | list of (alias \\| [Field](#field-node)) (1\u20132) | no | Up to two fields read under the title, in place of the table\'s |\n| `figure` | alias \\| `false` | no | The one number a row stands for, in place of the table\'s; false, none |\n| `image` | alias \\| `false` | no | A files field that pictures a row, in place of the table\'s; false, none |\n| `due` | list of alias (at least one) \\| `false` | no | Dates that count down, in place of the table\'s; false, none |\n| `status` | `false` \\| `{ "edit": true \\| false }` | no | false: the stage is not shown. `{ edit: true }`: the stage is also moved by hand. `{ edit: false }`: nobody moves it in this app, another app does. Absent, the stage\'s moves are buttons |\n\n#### List row title\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `edit` | `false` | no | false: the title is read; absent, it is written where the table lets a person write it |\n| `add` | `false` \\| `true` \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The new row this field\'s picker offers asks these fields; `true`: the target\'s required fields, its natural key and its title. Stated once per target table, and it holds in every picker of that table in the app. `false`: this link only picks, in every picker of it the app draws, not only where it is written |\n| `record` | list of alias (at least one) | no | The fields of the row this link names that this app saves from the link\'s card, which opens the row in the drawer: stored fields of the linked table a person writes. Stated once per linked table, and it holds on every card of that table in the app, where no List of the app draws its rows. Absent, the card reads the row |\n| `remove` | `true` | no | true: the row this link names is deleted from the link\'s card. Stated once per linked table, with its `record`, and it holds on every card of that table in the app. Absent, the card deletes nothing |\n\n#### Edit column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The column\'s field |\n| `edit` | `true` | yes | true: the column is the line\'s own control, written in place |\n\n#### Expect\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The child\'s title when the record holds one row per value of it: a single select (one line per option) or a one-row link to a catalog (one line per row) |\n| `of` | alias | no | With a select: the record\'s own multi-select holding the options this record expects a line for, in place of every option |\n| `only` | list of alias (at least one) | no | With a select: the options this List holds a line for; never with `of` |\n\n#### Compare\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `before` | alias | yes | A field of the List\'s rows holding what stood before: a files field (the photo of the place before the work), or a value \u2014 text, a number, a date or a single select (the price a row held before a sheet changed it) |\n| `after` | alias | yes | Another field of the same type holding what stands after: the photo of the same place once the work is done, or the value the row holds now |\n| `edit` | `true` | no | The after is written on the line |\n\n#### Tiered filter\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row |\n| `tiers` | list of number (at least one) | yes | The breakpoints between its bands, ascending, in the field\'s stored unit: under the first, from each to the next, from the last up \u2014 each band worded in the field\'s notation; a filter picks several at once |\n\n#### Sort key\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field rows are ordered by |\n| `desc` | `true` | no | Latest or largest first; absent, soonest or smallest first |\n\n<!-- generated:end node-list -->\n\nA List takes only the props its place lists:\n\n<!-- generated:start list-places -->\n\n| A List standing in | Takes |\n|---|---|\n| the register (the first List of the App) and a log (each further one) | `key`, `entity`, `row`, `label`, `columns`, `filters`, `search`, `group`, `sort`, `opens`, `period`, `attention`, `layout`, `create`, `add_label`, `remove`, `readings`, `where` |\n| a record\'s child rows, as a table, a checklist or a desk | `key`, `entity`, `via`, `label`, `columns`, `create`, `add_label`, `where`, `under`, `filters`, `search`, `group`, `sort`, `remove`, `pick`, `layout`, `compare`, `current`, `when`, `expect`, `row` |\n| a record\'s child rows with `layout` an agenda | `key`, `entity`, `via`, `label`, `columns`, `create`, `add_label`, `remove`, `layout`, `when`, `row` |\n| a record\'s child rows with `layout` `"timeline"` | `key`, `entity`, `via`, `label`, `create`, `add_label`, `remove`, `layout`, `when`, `row` |\n| a record\'s related rows, read only (`same`, `app`, or a Record that is `edit: false`) | `key`, `entity`, `via`, `label`, `where`, `columns`, `app`, `same`, `when`, `row` |\n| the documents a child List\'s lines stand under (`under`), a List inside it | `key`, `entity`, `create`, `remove` |\n| the cells of a roster, a List inside it | `key`, `entity`, `expect`, `create`, `remove`, `row` |\n| a dashboard\'s list, read only | `key`, `entity`, `label`, `where`, `columns`, `app` |\n\n<!-- generated:end list-places -->\n\n- **Children:** Tab, Reading, Progress, Export, Act, Check, Book, Record, and a List for a roster\'s cells. A Check or an\n Act stands in the List whose rows it works on: on each row (its \u22EF and the open row\'s), or with `on` on the ticked rows\n or every row in view.\n- **A child List\'s Record** holds `edit`, Fields and Lists only: its Fields are what the rows\' saves write, and a row\n opens in place under itself \u2014 its fields, then each List of its own children (an entity linking to the List\'s), stated\n as a child List one level up with its Acts, Checks and Record, at any depth. With `edit: false` the List only reads, as\n a related-rows List does: it states its `label`, takes none of `filters`, `search`, `group`, `sort`, `create`,\n `remove` or `layout`, and may reach rows linked to the record only through a row of their own (`via:\n "<link>.<link>"`).\n- **Documents:** in a child List with `under`, a List over the document\'s table (`key`, `entity`, `create`, `remove`,\n Acts, Checks and a Record of the document\'s Fields and Lists) is the one place the documents are added or deleted;\n `create` states `true`, and `remove` needs `create: true` beside it.\n- **Writes:** a List that writes its rows adds and deletes them \u2014 `create: false` and `remove: false` turn each off \u2014 `pick` links existing rows, an `edit: true` column writes that\n field on the line, and a layout move (a gantt bar, a lane\'s block, a plan\'s item) writes only fields the app already\n writes on the row\'s page.\n- **`create` asks:** `true` asks the title, the natural key and every required field a person writes \u2014 never a computed\n or stamped field, nor the link the place already sets. An `expect` list also asks the key it makes its lines by, and\n their picture. A list of fields overrides it; `false`, rows are not added here.\n- **`row`** overrides what the table\'s `records` say of the register\'s rows in this List alone (a cashier\'s figure is what\n is still owed): the subtitle, figure, image and deadlines stated replace the table\'s. A child List, a related List and\n a roster\'s cells read their rows as their table does, so their `row` states only a picker\'s add and a link\'s card (on its title or\n subtitle) or the stage moved by hand (`row.status`).\n- **`filters`** are the fields a reader narrows by every day, none to three \u2014 a quota never; each earns its place by\n daily use. A date filters by a range of days, a number by a range in its unit, `{ "field", "tiers" }` by the bands its\n breakpoints cut. A threshold the business names ("large orders") is a formula yes/no, never a range. The stage chips\n always stand.\n- **`opens`** is the view the register opens on, each entry a chip the reader may take away; absent, the open work\n (rows not at a `closed` stage).\n- **`expect`** makes one line per option of a select, or per row of a catalog link, and an empty line adds its row\n (a checklist of papers per case). **`under`** groups lines under the document that covers them.\n- **`compare`** reads each row\'s before against its after: two files fields as a pair of photos, or two values of one\n type as one column under the after\'s label.\n\n## Layouts\n\nA List\'s `layout` is how it draws its rows: a word (`"table"`, `"cards"`, `"calendar"`, `"roster"`, `"timeline"`) or an\nobject stating one layout and the data it reads (`{ "gantt": \u2026 }`, `{ "lanes": \u2026 }`, `{ "plan": \u2026 }`, `{ "agenda": \u2026 }`,\n`{ "checklist": \u2026 }`, `{ "desk": \u2026 }`). Absent, a table: the word `"table"` is the register\'s, and a child List states a\nchecklist, a desk, an agenda or `"timeline"`, else leaves `layout` out. Each layout reads what the rows already state \u2014 their\n`records` line, stage and dates \u2014 and the keys a layout adds stand on its section: here for an agenda and a checklist, \xA7 Time and place layouts for the rest, \xA7 Publish desk for a desk. Which one a job needs: the\n`design` reference. The register lays out as a table, cards, a calendar, a roster, a gantt, lanes or a plan; a record\'s\nchild rows as a table, a checklist, a desk, an agenda or a timeline.\n\n### Table\n\nAbsent `layout`, each row is one line: the table\'s `records` entry \u2014 its picture or mark, title and the line under it \u2014\nthen the stage, the `columns`, and the figure at the right; the title, the stage and a column of days or amounts sort\nthe rows by their header. A table groups its rows by `group`; a table or cards reads `Tab`s and `Book`s.\n\n### Cards\n\n`"cards"` draws each row as a card led by its picture \u2014 the `records` `image`, or the List\'s `row.image`: its title and\nthe line under it, its stage and what needs the reader (a deadline, a warning), then its figure and the `columns` at its\nfoot. Cards are for rows found by their picture (a product, a vehicle, a damage photo); rows with no picture read better\nas a table.\n\n### Calendar\n\n`"calendar"` places each row on its day \u2014 a month, a week, a day or a list (a phone reads the list or a day): at its\nhour where the date holds one, over its span where the row\'s line holds a second date after it, its line\'s other words\nand the `columns` after its title. The calendar reads the window in view, and its count and the stage chips count that\nwindow, opening on every row.\n\n### Roster\n\n`"roster"` draws the List\'s rows down the side and the days of a week or a month across; the roster\'s cells are a List\ninside it over the table whose rows fill the days \u2014 each stands on one row by its single link to it, and on the first\ndate of its line (a shift on a person\'s day, a run on a vehicle\'s), on every day through the second date where its line\nholds one. A cell reads its row\'s stage, else the first single select on its line (a shift\'s kind), several rows their\ncount. A day holding none stays empty: nothing was due, which is never an absence. A filled cell opens its row; an empty\none adds a row there, the register row and the day set. With `expect` on the cells (a single select of those rows) the\ncolumns are its options in place of days \u2014 a checklist of papers \u2014 and `of` names the ones each row expects: a gap among\nthem reads as one and adds its row, the others stand blank, and each row reads what it holds of them, `2/3`. Over days, a\nroster reads the `cadence` its table\'s `records` states (a site\'s diary page, Monday to Saturday from breaking ground to\nhandover): a past day owed with none is marked missing and counted, and each row reads the days it holds of those owed. A\nroster states no `columns` and needs no readings: its legend counts each state in view.\n\n```json\n{ "type": "List", "entity": "operator", "layout": "roster", "filters": ["depot"], "children": [{ "type": "List", "entity": "run" }] }\n```\n\n### Timeline\n\n`"timeline"` reads a record\'s child rows as a log of dated entries, newest first, with a composer where the app adds\nentries, each corrected and removed where it stands. It is never the record\'s status history: the rail shows each\nmove. A timeline\'s moment is its child\'s first date, and who its member field. Rows each what happened on a day and who \u2014\na date on their line, a member, no stage and no figure \u2014 read best as a timeline.\n\n### Agenda\n\n`{ "agenda": { \u2026 } }` reads a record\'s child rows as planned entries on a time axis: by day, soonest first, each with its\npicture and the day that is today marked. The day is the first date on the child\'s row line (its `records` title or\nsubtitle); within a day entries follow a datetime there, else the first single select after the date. An entry with no day\nyet is a plan, listed first and dated in place. `start` is the record\'s date that is the first day: each day\'s heading\ncounts from it (Day 1, Day 2).\n\n<!-- generated:start layout-agenda -->\n\n#### Agenda keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The record\'s date that is the first day: each day\'s heading counts from it; absent, the weekday and the date alone |\n\n<!-- generated:end layout-agenda -->\n\n### Checklist\n\n`{ "checklist": { \u2026 } }` reads a record\'s child rows, which are tasks, as a checklist of checks: each one\'s mark \u2014 passed\nat a `closed` option, failed at a `failed` one, else still to check \u2014 its title, its columns as its words (a text column\nits note, on its own line), and on a line under its title its photo and its clause. Never grouped nor written in place.\n`passed` alone sets the mark, so rows with no `task` and no status are checks: a measured yes/no is the verdict. With\n`expect` on the standard\'s items, each item is a line marked the same way (a missing one still to check), filled in\nplace by the block\'s `columns` (a measured value, a note, a photo), so `photo` and `clause` are not named.\n\n<!-- generated:start layout-checklist -->\n\n#### Checklist keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photo` | alias | no | The child\'s files field holding the photo each row is checked by; absent, the child\'s `records.image` |\n| `clause` | alias | no | The child\'s one-row link to what each row is checked against \u2014 a standard\'s clause, a rule |\n| `failed` | list of alias (at least one) | no | Options of the child\'s status a row stands failed at \u2014 none of them closed |\n| `passed` | alias | no | The child\'s yes/no field (stored or a formula) setting each row\'s mark: yes passed, no failed, empty still to check |\n\n<!-- generated:end layout-checklist -->\n\n## Time and place layouts\n\nThe layouts that place a List\'s rows in time or on a plan. Each is a `layout` object stating the data it reads.\n\n### Lanes\n\n`{ "lanes": { "by": \u2026 } }` draws each row of the linked table as a lane (a chair, a machine, a room), empty ones too,\nand each row of the List as a block from the first date on its line to the second: over the hours of one day where the\nfirst holds its hour, else over the days of one day, a week or a month. A block reads the row\'s name and the first of its\n`columns`; a free stretch between blocks adds a row there, its lane and its start set, and its end where the next block\nbounds it. Rows naming no lane stand first, in a lane of their own; where the app writes the link, each block moves to\nanother lane from its menu. Inside a `scope`, the lanes are the ones whose own one-link names that scope\'s row. A span\'s\nend holds its day unless `records` names it the day the row `frees`.\n\n`loads` names what a lane carries: `{ "weight": "payload", "volume": "box" }` sums each block\'s number and reads it\nagainst the lane row\'s own, at most two. A lane holds its blocks while they stand, so it reads the most they add up to at\none time in the window, named, past its bound, by how much and on which day or hour. The two are in one unit, or two of\none dimension. A bound a single row must keep (no parcel heavier than its van) is a `max` on the field through the link,\nwhich refuses the move.\n\n<!-- generated:start layout-lanes -->\n\n#### Lanes keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `by` | alias | yes | A one-link of this entity whose target\'s rows are the lanes (a chair, a machine, a room), each drawn even when empty |\n| `loads` | map of alias \u2192 alias | no | What a lane carries against what it holds, at most 2: a number of this entity each block adds \u2192 a number of the lane\'s row it is held within, in one unit or two of one dimension |\n\n<!-- generated:end layout-lanes -->\n\n### Gantt\n\n`{ "gantt": { \u2026 } }` draws each row as one bar on one axis of time: `{ "start": "etd", "end": "eta", "milestones":\n["cut_off"], "planned": { "end": "booked_eta" }, "progress": "done", "after": "waits_on" }`. `end` defaults to the table\'s\nfirst `due`, and a table with neither is refused; `start` and `end` are two dates of the row, both days or both moments,\nand each planned date is read as `start` is. `progress` is a percent (0\u2013100); absent, a row whose record lists its tasks\nfills to how many are done. `after` is a link to rows of the same table, each ending before the row starts. The bars\nstand in the lanes of the List\'s `group`. Where the app writes the row and a person writes a bar\'s date, the bar is\ndragged through the row\'s save; a computed date, one an Act sets, or a row the app does not write drags nothing.\n\n<!-- generated:start layout-gantt -->\n\n#### Gantt bar\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of this entity each row\'s bar starts at (a sailing, a hire\'s start, a task\'s start) |\n| `end` | alias | no | The date each row\'s bar ends at, read as `start` is \u2014 two days or two moments; absent, the entity\'s first `due` |\n| `milestones` | list of alias (at least one) | no | Dates of the row marked on its bar\'s line as diamonds, each named by its label (a cut-off, a delivery, the end of free time) \u2014 never `start` or `end` |\n| `planned` | [Gantt plan](#gantt-plan) | no | The plan each row is read against, drawn as a thin bar under its own \u2014 each date read as `start` is; a missing one is the bar\'s own |\n| `progress` | alias | no | A percent of this entity (0\u2013100) filling each row\'s bar as far as the work is done; absent, how many of the row\'s tasks are done fills it where its record lists them, else the bar wears its status\'s tone |\n| `after` | alias | no | A link of this entity to its own rows each row waits on \u2014 it starts once they end (finish-to-start), an arrow from each; a row starting before one ends is drawn in the danger tone |\n\n#### Gantt plan\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The date the row was planned to start at |\n| `end` | alias | no | The date the row was planned to end at (a booked arrival) |\n\n<!-- generated:end layout-gantt -->\n\n### Plan\n\n`{ "plan": { \u2026 } }` draws things that stand at a place \u2014 pallets in a warehouse \u2014 on a plan of the places. The places are\nthe rows of a table whose `records` states a `plan_place` (a bay, an open area or a landmark, each at its spot on a sheet\nof the plan); `at` names the one-link of the List\'s rows to a place and, for an item in a bay, the row (`across`) and the\ntier (`up`) it stands in, counted from 1. A bay is drawn as its rows and tiers, an area with a count, a landmark as a\nlabelled frame; the reader sees a plan, one bay enlarged, and the same plan in 3D. The List may read other tables as\n`Book`s: one plan of every table\'s items. Each item carries `__reach` \u2014 how many items stand in the way of taking it out\n(those above it where the place is `stacked`, and those in the rows between it and the side `reach_from` says the bay is\ntaken from) \u2014 so the list, its export and an agent\'s read all carry it; `<table>_plan_load` counts the rows each place\nholds. A move through the app is refused unless it clears the place, row and tier all three, or places the item at\na free cell of a bay \u2014 resting on a present item where the place is `stacked` \u2014 or at an area. Every other write (a grid edit, an import, a tool) holds one item to a cell only where the item\'s table states `unique: [<at>, <across>, <up>]`; a plan over a table stating none is noted. A plan opens on every row\nits `where` keeps, with no stage chip narrowing it. Rows with no place wait in a tray beside the plan. An Act that asks exactly a place (a link to the plan\'s places) and two numbers (the row and the tier) is answered by a pick on the plan, never by a form. On a desk a press opens a row\'s record in the panel beside the plan.\n\n```jsonc\n// table "bay"\n"title": "code",\n"records": { "plan_place": { "kind": "kind", "x": "x", "y": "y", "across": "rows", "up": "tiers", "reach_from": "taken_from", "stacked": "is_stack" } }\n```\n\n```json\n{ "type": "List", "entity": "pallet", "filters": ["line"], "layout": { "plan": { "at": "bay", "across": "row", "up": "tier" } } }\n```\n\n```jsonc\n// table "pallet": one item to a cell on every write, not only through the app\n"unique": [["bay", "row", "tier"]]\n```\n\n<!-- generated:start layout-plan -->\n\n#### Plan keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `at` | alias | yes | A one-link of this entity to an entity whose `records` states a `plan_place`: where each row stands |\n| `across` | alias | yes | A number field: the row of a bay a row stands in, counted from 1; empty at an area |\n| `up` | alias | yes | A number field: the tier of a bay a row stands at, counted from 1 at the ground; empty at an area |\n| `fill` | alias | no | A single select the items are coloured by; absent, the first single select among the register\'s filters |\n| `mark` | [Plan mark](#plan-mark) | no | A row holding the option says the option\'s label on its box and is hatched in the option\'s own colour, and the legend names it by the label; in books, the select is matched by option alias, and a book lacking the select never marks its rows |\n| `size` | [Plan size](#plan-size) | no | Each row\'s box as it stands in the world, in the one length unit its places\' `size` states: the option the row holds of the select names its box\'s width across, depth along and height; in books, matched by option alias, as `mark` is; a row holding an option it does not name is drawn at its place\'s cell |\n\n#### Plan mark\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A single select of this entity |\n| `option` | alias | yes | An option alias of that select |\n\n#### Plan size\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A single select of this entity |\n| `options` | map of alias \u2192 [Plan size box](#plan-size-box) | yes | The box of each option alias of that select |\n\n#### Plan size box\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `across` | number | yes | The box\'s width across the bay\'s rows |\n| `along` | number | yes | The box\'s depth along them |\n| `up` | number | yes | The box\'s height |\n\n#### Plan place\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `kind` | alias | yes | A single select, options aliased exactly `bay`, `area`, `landmark`: a bay holds rows and tiers, an area neither, a landmark nothing |\n| `sheet` | alias | no | The plan sheet it is on: a single select, or a one-row link to an entity whose `records` states its `image` \u2014 the sheet\'s drawing, which the places are drawn over (with `scale`) |\n| `scale` | alias | no | A number field of the sheet entity `sheet` links to: how many cells wide its drawing is, the plan\'s scale. Required where that entity states an `image`, and only then |\n| `x` | alias | yes | A number field: left edge, in cells |\n| `y` | alias | yes | A number field: top edge, in cells |\n| `width` | alias | no | A number field: an area\'s frame width, in cells |\n| `depth` | alias | no | A number field: an area\'s frame depth, in cells |\n| `across` | alias | yes | A number field: a bay\'s rows |\n| `up` | alias | yes | A number field: a bay\'s most tiers |\n| `reach_from` | alias | no | A single select, options aliased exactly `first`, `last`, `both`: the side boxes leave a bay from; a bay that names none has no reach and no arrow |\n| `stacked` | alias | no | A boolean field: true where items at this place rest on the one below (a stack), so a tier above the first needs a present item under it, shows floating without one, and an item\'s reach counts the items above it; false or empty, items sit at any tier and only the rows between a bay\'s reach side and the item are in the way |\n| `size` | [Plan place size](#plan-place-size) | no | The real size of ONE cell of the place, for 3D: three number fields stating one length `unit` alike. Absent, a cell is drawn at the size most of the sheet\'s places state, a unit cube where none states one |\n\n#### Plan place size\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `across` | alias | yes | A number field: the width of one cell across the rows |\n| `along` | alias | yes | A number field: the depth of one cell along the place |\n| `up` | alias | yes | A number field: the height of one cell, one level |\n\n<!-- generated:end layout-plan -->\n\n## Publish desk\n\nRows that are each a place the record is sent \u2014 a notice\'s boards, a listing\'s portals \u2014 are a record\'s child List with\n`expect` on the child\'s one-row link to the catalog of those places and `layout: { "desk": \u2026 }`:\n\n```json\n{\n "type": "List",\n "entity": "posting",\n "expect": "board",\n "columns": ["posted_link", "refusal"],\n "layout": { "desk": { "bodies": ["wording", "wording_abroad"], "text": "goes_out_as", "media": "pictures", "link": "page", "group": "reach", "network": "kind", "networks": { "company_page": "linkedin" } } }\n}\n```\n\nThe desk is where the post is written. `bodies` are the record\'s plain text fields it is written in (one per language,\nsay), each its editor between the list and the preview, saved as any field of the record is and drawn nowhere else on it.\nThe preview is `text` read over what is on screen: where `text` is a formula of the child, the desk computes that same\nformula over what is typed, so the post moves as it is written, and a place not ticked previews the words its row would\nhold. For that the formula reads only what a screen holds whole: stored values of its row, the record through lookups over\nthe link back to it, the place through lookups over `expect`, and formulas of the row over those \u2014 a rollup, a lookup\nthrough another link or the clock is refused, naming the field.\n\nEvery place the link admits is a line with a tick and that place\'s face: its picture, else the mark of the network it is on\nwhere `networks` names one. `group`, another single select of the place, stands the lines under its options in their\norder: a kind holding a ticked line is open, one holding none folded to its heading and its count. A tick makes the line\'s\nrow from its key alone \u2014 so nothing else the child requires is left without a default \u2014 and an untick deletes it until the\nrow\'s stage is `closed`: what went out is not taken back. It asks first only where a person wrote on the row. One line is\nworked beside the list and read as the post that place\'s feed shows: `text` is the child\'s words for that place, `media`\nand `link` the record\'s, and `network` the place\'s single select whose options `networks` names to a feed; an option not\nnamed reads on a plain card. Under the bodies stand the List\'s `columns` a person writes of the line\'s row; under the post,\nthe press copying its words beside the Acts of the child, then what the line and the `columns` hold that nobody writes\nthere (the link it went out at, why it was refused). A value an Act or a workflow fills is stated as that Act\'s, so the\ndesk never offers it as an empty box. Sending is an ordinary Act of the record.\n\n<!-- generated:start layout-desk -->\n\n#### Desk keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `bodies` | list of alias (1\u20133) | no | The record\'s plain text fields the post is written in, in order \u2014 one per language, say \u2014 at most 3: each its editor on the desk, between the list and the preview, and drawn nowhere else on the record |\n| `text` | alias | yes | A text of the block\'s rows \u2014 stored, or a formula yielding text \u2014 holding the words that go out on that row\'s destination. A formula is computed on the desk too, over what is typed: it reads stored values of its row, the record through lookups over the link back to it, the destination through lookups over `expect`, and formulas of the row over those \u2014 never a rollup, another link or the clock |\n| `media` | alias | yes | A files field of the record: the pictures or the video the post carries |\n| `link` | alias | no | A text field of the record holding a link (`format: "link"`), carried as the post\'s card |\n| `network` | alias | yes | A single select of the entity `expect` links to, saying which network each destination is on |\n| `group` | alias | no | Another single select of the entity `expect` links to \u2014 the destination\'s kind \u2014 the lines stand grouped by: each option a heading in the select\'s order, a group holding no ticked line folded to its heading and its count, destinations holding no option last |\n| `networks` | map of alias \u2192 `"facebook"` \\| `"instagram"` \\| `"threads"` \\| `"x"` \\| `"linkedin"` | yes | That select\'s options, each by its alias, to the network whose feed the post is previewed in \u2014 facebook, instagram, threads, x, linkedin. An option not named has no feed to imitate: its words read as a plain card |\n\n<!-- generated:end layout-desk -->\n\n## Record node\n\nA Record draws what a row opens: the head, then the parts.\n\n- **The head:** title, subtitle, picture, mark, party, stage badge, figure and meter, deadlines, description \u2014 always the\n table\'s `records` facts, overridden by the List\'s `row`. A field the head draws is never also a Field of the record.\n- **No Record node:** under the register, the platform designs the record\'s Sections from the rows: the fields the\n head does not draw, child Lists, papers and Acts. A stated Record stands exactly as written, so it replaces them.\n Under a child List, an absent Record is every field of the child except the link back.\n- **Children:** either parts \u2014 `Section`, `Side`, `Page` \u2014 or, for a one-part record, `Field`, `List`, `Reading`,\n `Progress`, `Statement` and `Act` directly. The two are never mixed. Below the register \u2014 a child List\'s, a document\'s\n or a roster cell\'s \u2014 a Record holds `edit`, the Fields its saves write and the Lists of its rows\' own children; a\n Book\'s holds Fields and `edit` only.\n\n<!-- generated:start node-record -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `door` | `"page"` \\| `"drawer"` \\| `"beside"` | no | Absent, a page where a part is a step, a Side or a Page or there are more than 3 parts, else a drawer |\n| `comments` | `true` | no | The record carries comments; absent, nobody comments |\n| `edit` | `false` | no | false: nothing the record draws is written |\n| `compose` | [piece](#composed-page) | no | The page stated as a tree of finished pieces; a Record with `compose` holds no children |\n| `children` | list of (alias \\| [Field](#field-node) \\| [Section](#section-node) \\| [Side](#side-node) \\| [Page](#page-node) \\| [Outside](#outside-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | Parts (Section, Side, Page), or for a one-part record its Fields, Lists, Readings, Progress, Statements and Acts directly \u2014 never both; Outside parts stand beside either. Absent, the platform designs the Sections from the rows: the fields the head does not draw, child Lists, papers and Acts. Stated, it stands exactly as written |\n\n<!-- generated:end node-record -->\n\nThe door is a page where a part is a step, a Side or a Page, or where there are more than 3 parts; else a drawer. `door`\nis a judged choice of how a job is worked (one after another over a long record), so it is kept. From three parts on, the\nrecord is navigated by their titles: a page by a rail that jumps to each (a strip of the same names on a phone), a drawer\nby the same strip pinned over its scroll; with fewer, the parts just stack.\n\n`comments: true` lets people comment, and is opt-in: state it only where the job\'s people discuss a record. A door in the\nrecord\'s head then opens the comments in a panel beside it, with one composer. A record shows no activity and no audit\nlog: a history an app shows is table-backed \u2014 `records.status.dates`, `records.status.history`, an event table an act\nmakes \u2014 stated where the app shows it.\n\nA field, a Reading and an Act is each placed once on a record: the head, or one part. A Section or a List with `when` is\ndrawn only while each named select of the record holds one of its options.\n\nA page the parts cannot say is a **composed page** (\xA7 Composed page): `compose` states the record as a tree of finished\npieces, and the Record holds no children.\n\n## Section node\n\nA titled part of a record, and optionally a step of its path. A Section draws its Fields, Lists, Readings, Progress,\nStatements and Acts in reading order; its Acts stand at its foot.\n\n<!-- generated:start node-section -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique among its siblings; part of the node\'s address. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The part\'s heading |\n| `description` | text | no | A line under the heading |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `edit` | `false` | no | false: nothing in this part is written |\n| `at` | list of alias (at least one) | no | Options of the record\'s status during which this part is the work now: a step of its path |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | What the part draws, in reading order; its Acts stand at its foot |\n\n<!-- generated:end node-section -->\n\n- **`at`** names the stage options during which the section is the work now \u2014 only on a record that\n truly moves through those stages: the rail marks it done, now or waiting, and the section stands even holding nothing\n yet. It never hides a field, locks it or gates an act.\n- **Acts:** every Act on a record stands in the Section whose work it is. An Act moving the stage and offered at ONE step\n stands at that step\'s foot when it moves the work forward, to an exit later in the stage\'s options, or back to a step the\n work passes anyway; one offered at several steps, or moving back into a detour, stands in the Section it enters. A\n correction (`danger`, and not one of its step\'s outcomes) waits in the Section heading\'s \u22EF.\n- **A detour** \u2014 a stage the work only steps aside into from one step and leaves only back to it (a pause, a repair) \u2014\n stands before that step, its option and its Section alike.\n- **An exit** stands only once the record is in it, its reason there and its rail item apart from the path; until then the\n Act waits in the header\'s \u22EF.\n- **Writes:** its Fields and its Acts.\n\n## Side node\n\nThe one part drawn beside the rest \u2014 the photos looked at, the pool picked from. At most one Side to a record, on a page\ndoor, and never also a Page.\n\n<!-- generated:start node-side -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique among its siblings; part of the node\'s address. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The part\'s heading |\n| `description` | text | no | A line under the heading |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `edit` | `false` | no | false: nothing in this part is written |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | What the part draws, in reading order; its Acts stand at its foot |\n\n<!-- generated:end node-side -->\n\nIt holds what a Section holds. Writes: its Fields and its Acts.\n\n## Page node\n\nA part on its own page, off the record\'s scroll, with a way back \u2014 rarely, for a long related list or a workspace of its\nown that would make the record worse stacked. Never on a drawer, and never a step: its title names an address no other\npage shares (a letter or a digit at least), and not every part is a Page.\n\n<!-- generated:start node-page -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique among its siblings; part of the node\'s address. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The part\'s heading |\n| `description` | text | no | A line under the heading |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `edit` | `false` | no | false: nothing in this part is written |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | What the part draws, in reading order; its Acts stand at its foot |\n\n<!-- generated:end node-page -->\n\nIt holds what a Section holds. Writes: its Fields and its Acts.\n\n## Outside node\n\nWhat one record shows, and which acts it allows, to a person with no seat who opens a link a member made of it. A member\npresses Create link on a record while its status stands in `at`; the link opens that one record until it expires, is\nrevoked, or its act moves the status out of `at`. The person sees the stated Fields and Lists and presses the stated Acts\nand nothing else. It stands directly in a Record of a table with a stored status select: the register\'s, or the Record of\na List below the register, whose rows the link then opens.\n\n<!-- generated:start node-outside -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique in the app: a link to the record names it. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The heading of the page the person without a seat opens |\n| `description` | text | no | A line under the heading |\n| `at` | list of alias (at least one) | yes | Options of the record\'s status during which a link to it opens; none is owned by a stage, and each Act moves the status out of them |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Act](#act-node)) (at least one) | yes | Exactly what the link shows and allows: Fields read as values, Lists of child rows read as they are, and the Acts the person may press, each moving the status out of `at` |\n\n<!-- generated:end node-outside -->\n\n- **Fields** are read as values, in order; a lookup, a rollup or a formula is computed for the link only if stated here.\n A Field states nothing but its alias.\n- **Lists** read the child rows under the record as they are: `columns`, `label` and `where`.\n- **Acts** are the link\'s alone: no member, visitor or agent presses them. Each states `key`, `label`, `confirm`, `when`,\n `requires`, `asks`, `set`, `template` with `into`, or `sign`. `when` absent is the status held in `at`; `set` moves the\n status to an option outside `at`, so each press ends the visit. `asks` are text, numbers, dates or listed options. A\n `sign` is by a text field the signer types, with no witness: the link is the attestation.\n- `at` names options no stage owns, and no two Outsides of a table open during the same option.\n- **Writes:** its Acts, and nothing else.\n\n## Tab node\n\nA Tab is one view of a List\'s rows over a period, or one sheet of a dashboard.\n\n- **In a List:** a register reporting a period \u2014 what stands at its end beside what came and went during it (the stock,\n the hires out, the tickets open) \u2014 reads its rows as Tabs over one `period` the reader picks above them. A Tab holds\n the rows `dated` within the period, those `open` at its end, or lists every row and draws what a child\'s rows came to\n in the period (`totals`). A Tab narrows its rows and holds no nodes; Readings stand beside it in the List and read the\n open Tab, or the one their `tab` names. Never date chips set by hand, and never a Tab per stage, which the chips split.\n- **On a dashboard:** a Tab holds its Readings and Lists as children. A dashboard with Tabs (2 to 5) draws a strip under\n its title that picks one, the first open.\n\n<!-- generated:start node-tab -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | Unique among its siblings; part of the node\'s address |\n| `label` | text | yes | What the tab holds, in the reader\'s words |\n| `dated` | alias | no | A date: the tab holds the rows dated within the List\'s period |\n| `open` | [Tab open](#tab-open) | no | The tab holds the rows open at the period\'s end: begun, and not yet ended |\n| `totals` | [Tab totals](#tab-totals) | no | Each row\'s figures over a child\'s rows in the period |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order this tab\'s rows read in; absent, the List\'s |\n| `children` | list of ([Reading](#reading-node) \\| [List](#list-node)) (at least one) | no | On a dashboard: the Readings and Lists this sheet draws. A Tab of a List holds none: Readings stand beside it in the List |\n\n#### Tab open\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The date a row begins on |\n| `to` | alias | yes | The date a row ends on, empty while it is open |\n\n#### Tab totals\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | An entity of this model whose rows each belong to one row of the register\'s entity: the rows the columns add |\n| `on` | alias | yes | A date of the child placing each row in time: stored, a formula yielding a date, or a lookup of a date through a one-row link \u2014 never a moment or a span of days |\n| `columns` | list of [Totals column](#totals-column) (1\u20136) | yes | The figures drawn in place of the register\'s columns after each row\'s name, each under its own alias and label |\n\n#### Totals column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The column\'s name: its address in the totals read |\n| `label` | text | yes | What the column holds, in the reader\'s words |\n| `value` | alias | no | A number of the child (stored, a formula or a rollup) summed over the rows; absent, the rows are counted. A figure read in each row\'s own unit or currency stands only where the child\'s select is a lookup, through `via`, of a single select of the register\'s entity |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the child\'s rows whose select holds one of these options, or whose yes/no is this value \u2014 every entry ANDed |\n| `window` | `"before"` \\| `"in"` \\| `"through"` | yes | Which of the child\'s rows, by `on`, the column adds: `before` those dated before the period\'s first day, `in` those from its first day through its last, `through` those on or before its last day |\n\n#### Period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | integer | yes | How many days through today, at most 366 |\n\n<!-- generated:end node-tab -->\n\nA `totals` Tab adds, per column, the child\'s rows `before` the period\'s first day, `in` the period, or `through` its last\nday, so `before` plus `in` of one value is its `through`:\n\n```json\n{\n "type": "Tab",\n "key": "items",\n "label": "Items",\n "totals": {\n "rows": "ledger_line",\n "on": "moved_on",\n "columns": [\n { "alias": "opening", "label": "Opening", "value": "quantity", "window": "before" },\n { "alias": "received", "label": "Received", "value": "quantity", "where": { "kind": ["receipt"] }, "window": "in" },\n { "alias": "issued", "label": "Issued", "value": "quantity", "where": { "kind": ["issue"] }, "window": "in" },\n { "alias": "closing", "label": "Closing", "value": "quantity", "window": "through" }\n ]\n }\n}\n```\n\n## Field node\n\nA Field draws one field of the record it stands in. How it is drawn follows from the field\'s type and the table\'s facts:\n\n- a control or a value, with its notation, money, units and deadline countdown;\n- a files field as pictures and documents, a long or markdown text as prose written in place \u2014 a gallery or a prose block,\n with a heading (`label`) and a condition (`when`) of its own; a run of files Fields is one gallery;\n- a link as its row\'s chip, with a picker narrowed by `options_where`, and `add` for the new row the picker offers;\n- the back side of a child\'s link as the rows filed under the record, added to with `add`;\n- the fields an Act\'s `sign` writes as one signature.\n\n<!-- generated:start node-field -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field\'s alias |\n| `edit` | `false` | no | false: the field is drawn and read; absent, it is written where the table lets a person write it |\n| `label` | text | no | The heading of the field\'s gallery or prose block, where it draws as one; absent, the field\'s label. A run of files fields takes the first one\'s |\n| `block` | boolean | no | true: a files or text field draws as its own gallery or prose block; false: as a control among the part\'s fields. Absent, files and markdown fields draw as blocks and plain text as a control |\n| `when` | [condition](#app-tree) | no | A gallery or prose block is shown only while the record\'s selects hold these options, its yes/nos these values, its numbers and dates meet their bounds and its `{ "empty": true \\| false }` fields are blank or filled |\n| `add` | `false` \\| `true` \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The new row this field\'s picker offers asks these fields; `true`: the target\'s required fields, its natural key and its title. Stated once per target table, and it holds in every picker of that table in the app. `false`: this link only picks, in every picker of it the app draws, not only where it is written |\n| `record` | list of alias (at least one) | no | The fields of the row this link names that this app saves from the link\'s card, which opens the row in the drawer: stored fields of the linked table a person writes. Stated once per linked table, and it holds on every card of that table in the app, where no List of the app draws its rows. Absent, the card reads the row |\n| `remove` | `true` | no | true: the row this link names is deleted from the link\'s card. Stated once per linked table, with its `record`, and it holds on every card of that table in the app. Absent, the card deletes nothing |\n\n<!-- generated:end node-field -->\n\nA field drawn in the head (title, subtitle, picture, figure, stage) is never also a Field node. A Field is drawn once per\npage. **Writes:** the field, under \xA7 App tree, and with `add` the target table\'s create.\n\n## Act node\n\nAn Act is a verb, stated whole where it stands. Its place decides what it works on:\n\n- in a Section, Side or Page, or in a one-part Record: one record, at that part\'s foot;\n- in a List: on each of its rows, offered in the row\'s \u22EF and, once the row is open, in its header\'s \u22EF; in a child List,\n the child\'s rows;\n- in a composed page\'s `Rows` (\xA7 Composed page): on each row of the child it lists, as in a child List;\n- in a List with `on`: the ticked rows (`"ticked"`) or every row in view (`"view"`), each checked before any is written;\n- directly in a Record that holds parts: refused.\n\n<!-- generated:start node-act -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The act\'s name; its workflow is `act_<key>`, or the `workflow` it names |\n| `label` | text | yes | The verb, as the reader says it |\n| `on` | `"ticked"` \\| `"view"` | no | In a List: the ticked rows, or every row in view; absent, one row |\n| `when` | [condition](#app-tree) | no | The act works only while each named select holds one of these options, each yes/no this value, each number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and each `{ "empty": true \\| false }` field is blank or filled |\n| `requires` | list of alias (at least one) | no | Fields that must be filled first, each drawn where the act stands; a blocked act names each one missing |\n| `recommends` | list of alias (at least one) | no | Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank |\n| `asks` | list of (alias \\| `{ "field": alias, "starts": "today" \\| "me" }` \\| [Asked input](#asked-input)) (at least one) | no | What the reader states when pressing it: fields of the row (written by the act) or inputs |\n| `set` | map of alias \u2192 [value](#app-tree) | no | What the act writes; each field is the act\'s, read-only elsewhere in the app |\n| `workflow` | alias | no | The workflow the act makes and runs under this name, for what `set` cannot say; no Workflow node is declared for it, and `set_app_workflow` sets its body |\n| `writes` | list of alias (at least one) | no | With `workflow`, fields of the acted row its body writes (an id kept from a service\'s answer) \u2014 the act\'s, as what `set` states is |\n| `template` | alias | no | A document template the act makes a file from: an html one made a PDF, an excel one a workbook, a word one a .docx; over several rows, one file of them all |\n| `templates` | list of [Act template](#act-template) (at least 2) | no | Several papers the act makes into `into`, instead of one `template`: the reader ticks which to make, one press making those |\n| `into` | alias | no | The record\'s files field the made document is kept in; remade, it replaces its own last paper, never a person\'s file |\n| `paper` | text | no | Its paper, `<child>.<kind>`: the line of the record\'s papers it stands on |\n| `intake` | alias | no | A child the record lists whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child |\n| `fills` | list of text (at least one) | no | With `intake`, what the papers read may fill: a field of the record, `<link>.<field>` on the row a required one-row link names, or a child whose rows the papers add. With `record`, fields of the new row an agent fills from the transcript |\n| `record` | [Recording](#recording) | no | The press records a call, visit or meeting and files it as a new row of a child the record lists |\n| `sign` | [Signature](#signature) | no | The press opens a pad the signer signs on; the act keeps the signature as a picture with who signed, when and the witness |\n| `open` | text | no | The id of another app that reaches this row\'s table, opened at this row by the press: navigation only \u2014 no workflow runs, and a reader without `use` on it is shown that app\'s own refusal. Stated alone with `label` and `when` |\n| `review` | `true` | no | With `workflow`: the workflow only proposes a changeset of the act\'s table, and the reader ticks the rows to write |\n| `confirm` | `true` \\| text | no | The press asks first: the question, or `true` for the act\'s label over the row |\n| `danger` | `true` | no | The act destroys or cannot be undone |\n\n#### Asked input\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `input` | alias | yes | A value the act asks for that is not a field of the record |\n| `label` | text | yes | What the reader is asked, in their words |\n| `type` | `"text"` \\| `"long_text"` \\| `"number"` \\| `"date"` \\| `"select"` \\| `"link"` \\| `"member"` \\| `"file"` | yes | What the reader states. A `file` is one uploaded file, handed to the act\'s authored `workflow` as its file id and read by nothing else |\n| `options` | list of [Select option](#select-option) (at least one) | no | The choices a `select` input offers, one picked \u2014 written into a select holding the same option aliases |\n| `entity` | alias | no | The entity a `link` input picks one row of |\n| `accepts` | list of `"sheet"` \\| `"pdf"` \\| `"document"` \\| `"photo"` (at least one) | no | The kinds of file a `file` input takes; absent, a sheet, a PDF or a document \u2014 a phone offers its camera only where `photo` is among them |\n| `required` | `true` | no | The act is refused without it |\n| `default` | alias | no | A field of the row whose value the input starts from, still changed at will |\n| `add` | `true` \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The new row this picker offers asks these fields; `true`: the target\'s required fields, its natural key and its title. Stated once per target table, and it holds in every picker of that table in the app |\n\n#### Set formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | text | yes | A formula over the acted row\'s fields, `{alias}` each, read as the entity\'s own formulas read them |\n\n#### Act template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `template` | alias | yes | A document template the act makes a paper of, kept in `into` \u2014 an html one made a PDF, an excel one a workbook, a word one a .docx |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked |\n\n#### Recording\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | A child the record lists in a timeline or rows block, linking back to it by a single link: each recording is filed as a new row of it |\n| `audio` | alias | yes | The child\'s files field the recording\'s audio is kept in |\n| `transcript` | alias | yes | The child\'s text field the transcript is kept in, a speaker\'s turn per line |\n| `video` | alias | no | The child\'s files field the screen is kept in, where the recording captured it |\n| `at` | alias | no | The child\'s date field stamped with the moment the recording started |\n| `by` | alias | no | The child\'s member field set to the person who recorded |\n\n#### Signature\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | The record\'s files field the signature is kept in, as a picture \u2014 each signing replaces the one before |\n| `by` | alias | yes | Who signed: a text field the signer types their name into (someone with no seat in the workspace \u2014 a driver, the owner\'s supervisor), or a member field set to the person pressing, who signs as themselves |\n| `at` | alias | yes | The record\'s date field stamped with the server\'s time as the act runs |\n| `witness` | alias | no | Beside a text `by`, the member field set to the person pressing, who saw the signer sign |\n\n<!-- generated:end node-act -->\n\nAn Act whose `when` does not hold is not offered. One whose `requires` are empty is offered disabled, naming each; one whose\n`recommends` are empty names what it will leave blank and still presses; one a standing Check blocks is offered disabled in\nthe check\'s words. The generated `act_<key>` workflow re-checks all three on the server, writes `set` and what was asked,\nand appends the status history where the act moves the status. A field an act asks is read-only everywhere else. A `select`\ninput offers its own options and is written into a select holding the same option aliases; a `link` input picks one row of\nits `entity`, a `member` input one person. A `file` input is one uploaded file handed to the act\'s authored `workflow` as its\nfile id; nothing else reads it. A field an act both `requires` and `asks` never disables the press.\n\n- **Writes:** every field it sets, asks, files into, signs or records is the act\'s.\n- **Papers.** A button that makes a paper is `template` (or `templates`) with `into` on the Act, with no workflow. `template` makes a document from the row as the act left it and keeps it in `into`; over several rows it\n makes one document of them all. `templates` names a set of papers kept in `into`, each made one as its file and each not\n made yet as a placeholder, those whose `when` holds ticked to start; the press makes the ticked ones, each replacing the\n file its template made before, and the set downloads as one archive (`archive_<key>`). `intake` reads papers: the press\n takes files, an agent reads them, and each is filed as the record\'s line of its kind in the named child; what they state\n of each field `fills` names is shown beside what the row holds, and the save writes only what differs. `sign` opens a pad\n on the device: the act keeps the picture in `into` in place of any before it, `at` stamped with the server\'s time and the\n person pressing in `witness`; the record reads `into` as one signature, and the act\'s paper is handed the picture under\n `into`\'s name (`{{#each signature}}<img src="{{url}}">{{/each}}` in an html template). `record` records a call or a visit\n through the host and files its audio, transcript and screen as a new row of a child.\n- **`workflow`** is an authored workflow (`workflows`) the act runs after its checks, for what neither `set` nor `template` can say.\n **`review: true`** makes it one that only proposes: its body reads, returns a changeset of the act\'s table in\n `data.review`, and the platform writes the rows the reader ticks in the act\'s dialog, each checked first and all written\n in one transaction or none. Over every row in view the act is handed no rows.\n- **A `when` that compares** splits one stage by an amount or a date, with no formula field behind it. The server refuses a\n press the row\'s numbers and dates do not meet, in the words "Approve works only while Amount is at most 5,000,000", and\n a blank number or date meets no bound. Two acts on an expense claim, the second moving it to a stage the Director group owns\n (stage owners decide who presses):\n\n```json\n{ "type": "Act", "key": "approve", "label": "Approve", "when": { "status": ["submitted"], "amount": { "lte": 5000000 } }, "set": { "status": "approved" } }\n```\n\n```json\n{ "type": "Act", "key": "send_to_director", "label": "Send to the director", "when": { "status": ["submitted"], "amount": { "gt": 5000000 } }, "set": { "status": "director_review" } }\n```\n\n```json\n{ "type": "Act", "key": "gate_in", "label": "Make the gate-in EIR", "template": "eir_in", "into": "eir", "sign": { "into": "driver_signature", "by": "driver_name", "at": "signed_at", "witness": "signed_before" } }\n```\n\n## Check node\n\nA Check is a yes/no or text formula of the rows of the List it stands in: it stands while true or non-empty, and a text\nformula\'s words are what the reader sees. Without `on` it only warns; with it, it refuses the saves and acts it names, on\nthe server, in every write it names.\n\n<!-- generated:start node-check -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A yes/no or text formula of the rows of the List it stands in: true or non-empty means the check stands |\n| `on` | `"all"` \\| list of (alias \\| `"update"` \\| `"delete"`) (at least one) | no | What the standing check refuses: `update` is the rows\' saves, an Act key is that act, `all` is every act and save; absent, it only warns. `delete` is this app\'s removal of the rows, for a rule no check of the table states |\n| `resolve` | alias | no | An Act on the same rows that clears it |\n\n<!-- generated:end node-check -->\n\n`update` is the rows\' saves; an Act key is that Act, on the same rows; `all` is every act and save.\nA rule that refuses a Delete is a check of the table, not a Check: state it in the table\'s `checks` with `on: ["delete"]`\n(\xA7 Write rules), where it holds for every way a row is deleted and a removal that cascades to child rows is refused whole.\nA Check naming `delete` refuses only this app\'s removal of a row, and is kept for a rule the table\'s checks cannot state (a\nformula that is neither a yes/no nor a text).\nA Check in a child List stands on each child row. A warning reading the figure of a meter that `gates` marks against its\nbound or its pass mark is refused: the meter already draws it.\n\nThe check field\'s `label` is its short name \u2014 "Over budget", "Release expired": a row\'s line says it in the check\'s tone\n(the most severe of several, and how many more), while its words whole are the row\'s tooltip. Label it as the reader names\nthe problem, in a few words, and let a text formula say the sentence. A check with no `resolve` links to the first field its formula reads that the record draws.\nA yes/no formula whose yes needs attention (papers the customer still owes) is a Check: its yes reads in the warning tone\nwherever it is drawn, every other yes in the success tone \u2014 and the row\'s line says it, so it is no column of the List too.\n\nA Check that every app over a table states the same way restates the table\'s contract and belongs in the table\'s `checks`\n(\xA7 Write rules).\n\nA Check refusing `update` (or `edit: false` on a record, part or field) makes the rows read-only in this app alone, drawn\nunder the read-only mark, never as a lock. A lock is the table\'s: a table workflow, a person, or an Act whose workflow calls\n`lock_records` locks a row, and every door refuses its writes until a table manager unlocks it \u2014 `unlock_records` in an\napp\'s run is judged as the member who pressed. A locked row reads locked in every app: its lock mark, its fields as values\nunder one line naming who can change it, its saves, Delete and each Act writing it refused \u2014 an Act that writes\nelsewhere, makes a paper it hands back, or unlocks it stays its workflow\'s to answer.\n\n## Reading node\n\nA Reading is a number over rows: one figure, its parts, a grid, a figure per period, steps; or a record\'s own figure. The\npicture is the runtime\'s, chosen from what the reading means \u2014 no prop picks a chart. The question is stated by which\nsplit it names:\n\n| Names | Answers |\n|---|---|\n| nothing | one figure: the rows counted, or `value` summed, `{ "avg": \u2026 }` averaged |\n| `by` | the figure per value of a select, one-row link, member or yes/no, or per band of a number\'s `tiers` |\n| `by` a date field | the figure per period (a trend); `ahead: true` reads forward over the rows still to come |\n| `by` and `across` | a grid, `by` down the side and `across` along the top |\n| `steps` | a funnel: 2 to 8 number fields summed, each kept as a share of the one before; in a record only |\n| no `entity`, in a Record | the record\'s own `value`, a tile under its field\'s label; two to eight consecutive tiles draw as one strip, and a single one is a field of the section |\n\n<!-- generated:start node-reading -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The reading\'s query name, unique in the app; a record\'s own figure is keyed by its field |\n| `label` | text | no | Absent, a split\'s `by` field\'s label, a trend\'s summed `value`\'s (else the rows\'), a grid\'s two fields\'; one figure states its label |\n| `entity` | alias | no | The List\'s rows; in a Record, the child\'s rows under it; absent in a Record with no `by`: the record\'s own `value` |\n| `via` | alias | no | The child\'s link to the record, where it has several |\n| `value` | alias \\| `{ "avg": alias }` | no | A number field summed over the rows, or `{ avg }` averaged; absent, the rows are counted |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, whose number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), whose field is `{ "empty": true \\| false }`, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `by` | alias \\| [Tiered filter](#tiered-filter) | no | Splits the rows into parts; a date field splits them per period |\n| `across` | alias \\| [Tiered filter](#tiered-filter) | no | With `by`: the columns of a grid |\n| `over` | alias | no | The date that places a row in time |\n| `ahead` | `true` | no | With a date `by`: the current period and those after it |\n| `steps` | list of alias (2\u20138) | no | Number fields summed, each a step of a funnel |\n| `on_hand` | [Reading on hand](#reading-on-hand) | no | With `by` (and `across`): the Reading\'s rows are what is owed, per value of those fields; drawn beside what is on hand per the same values, and the shortfall |\n| `target` | number | no | The number the figure is read against; absent, its bound, where the table states one |\n| `better` | `"up"` \\| `"down"` | no | Which way is good, for the figure\'s tone; absent, read from the figure\'s shape |\n| `tab` | alias | no | The Tab it reads, else the open one |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n\n#### Reading on hand\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | The entity whose rows are what is on hand, all of them |\n| `value` | alias | no | A number field summed over those rows; absent, the rows are counted |\n| `by` | alias | no | The field of this entity matching the Reading\'s `by`; absent, the same alias |\n| `across` | alias | no | The field of this entity matching the Reading\'s `across`; absent, the same alias |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, whose number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), whose field is `{ "empty": true \\| false }`, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n\n<!-- generated:end node-reading -->\n\nWhere a Reading stands decides which rows: a dashboard\'s every row, a List\'s the rows in view (so the search, the stage and\nthe filters narrow each one), a Record\'s the child\'s rows under the record. A Reading counts its rows, or sums the number\n`value` names \u2014 never a percent. `over` is a date, the row\'s own or its parent\'s through a lookup: a trend\'s periods, a\nfigure\'s recent periods, and what a dashboard\'s period windows. `where` keeps rows whose select holds one of the options, or\nwhose yes/no is the value, every entry ANDed \u2014 a band leads with the job\'s exception as a formula\'s yes/no (what is\nshort), never a stage the chips already count nor the overdue rows the register\'s summary line counts from `records.due`.\n\n- **A figure\'s change** is green where it moves the good way: a sum\'s rise, a count kept by `where` falling; `better` states\n the other way where that reads wrong (money going out). A value `limits` bounds reads against the sum of its bound\n (`10 / 14`); `target` reads one against a number nothing bounds.\n- **An average** is its sum over the rows that hold a value, divided by how many hold one; a row holding none is no part\n of it, and where none holds one the figure reads a dash, never 0. Under a dashboard\'s period it is the average over the\n period so far, never a sum of averages. A share is an average too: a number formula that is `100` on the rows that count\n and `0` on the others, in percent notation, averaged. Only a single figure averages.\n- **Bands.** `by` and `across` each take, in place of a field, a number by its `tiers` \u2014 `{ "field", "tiers" }` \u2014\n splitting the rows by the bands those breakpoints cut: `[1, 31, 61]` over days late reads not due, 1\u201330, 31\u201360 and over\n 60, the aged report. Every band is drawn in the bands\' own order, one holding no row at zero; rows holding no number\n stand apart. On a List a band is a door: a press narrows the rows to it. In a List\'s band, a split by one or two\n breakpoints stands in it, and one by three or more stands whole under it.\n- **In a List,** Readings stand under the title in one compact band: the job\'s headline number first, then at most the mix\n and the movement. A picture stands only where it splits the rows in view into two parts or more, one holding two rows or\n more. The summary line already totals the figure and the chips count the rows, so a figure summing the figure, or counting\n the rows of a table with a stage, is refused unless its `where` narrows it.\n- **Name.** A Reading\'s `key` is the query it is read by (`run_app_query`).\n\n## Progress node\n\nProgress reads one parent\'s work against its plan \u2014 a List\'s band over a folder, or a record\'s block, never a dashboard\'s\nevery row: the share of the work planned and the share done, cumulative day by day, today\'s two at its foot. Each row is a\npart of the work, its plan spread evenly over its planned days and weighing its `weight` (its value) or, absent, its planned\ndays. Its share done today is `done`, a percent the server computes; on an earlier day, its `done` from its `finished` day\non, before it what its `measured` rows add up to by then.\n\n<!-- generated:start node-progress -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The reading\'s query name, unique in the app |\n| `label` | text | yes | What the progress is of, in the reader\'s words |\n| `entity` | alias | yes | The rows that are the parts of the work: the List\'s, or in a Record the child\'s rows under it |\n| `via` | alias | no | The child\'s link to the record, where it has several |\n| `planned` | [Progress planned](#progress-planned) | yes | The days each row\'s work is planned over, its share planned spread evenly across them \u2014 a row missing either is no part of the plan, and neither line counts it |\n| `done` | alias | yes | A percent field of the row the server computes: its share done today (0\u2013100) |\n| `finished` | alias | no | A date of the row: the day its work was finished, from which it stands at its `done` |\n| `weight` | alias | no | A number of the row: what it weighs in the whole; a row holding none weighs nothing. Absent, each row weighs its planned days |\n| `measured` | [Progress measured](#progress-measured) | no | Before a row is finished, its share done on a day is what these add up to by then, never past its `done`; absent, a row stands at nothing until it is finished, and at its `done` today |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, whose number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), whose field is `{ "empty": true \\| false }`, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `tab` | alias | no | The Tab it reads, else the open one |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n\n#### Progress planned\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of the row: the first day of its planned work |\n| `end` | alias | yes | A date of the row: the last day of its planned work |\n\n#### Progress measured\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows record the work as it is done, each through its one-row link to a row of the reading |\n| `on` | alias | yes | A date of those rows (their own, or a lookup): the day each records its work |\n| `share` | alias | yes | A number of those rows the server computes: the share of its row each adds, in percent |\n\n<!-- generated:end node-progress -->\n\n## Statement node\n\nA Statement draws a figure the record works out from its own figures \u2014 a claim\'s payable, an invoice\'s total, a payslip\'s\nnet \u2014 line by line in the order the arithmetic runs: each line a number of the record, the first stating no `op`, the last\nan `equals`. Every figure is a field the server computes or a person writes; the screen adds nothing up. A line\'s `parts`\nlists the rows its figure carries, each read from its own row, and that figure is what they add up to \u2014 a formula, rollup\nor lookup, or the server\'s copy when the record is made (`default_from`), which nothing writes after.\n\n<!-- generated:start node-statement -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The statement\'s name, unique among its siblings |\n| `label` | text | no | Absent, the label of what the statement comes to |\n| `lines` | list of [Statement line](#statement-line) (at least 3) | yes | The figures it is built from, top to bottom, ending in the one it comes to |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n\n#### Statement line\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number of the record \u2014 stored, a formula, a rollup or a lookup \u2014 read on the line after its label |\n| `op` | `"plus"` \\| `"minus"` \\| `"times"` \\| `"equals"` | no | How the line works on the lines above it \u2014 `plus`, `minus`, `times`, or `equals` for what they come to, a formula, rollup or lookup the server computes; absent on the first line, which the rest work from |\n| `rate` | alias | no | A percent of the record read after the line\'s label: the rate its figure is taken at (retention at 5 %) |\n| `parts` | [Statement parts](#statement-parts) | no | The rows the line\'s figure carries, each listed under it with its words and its own figure \u2014 read from those rows, never a copy of them. The line\'s figure is what they add up to, drawn read: a formula, rollup or lookup, or a value the server copies when the record is made (`default_from`) that nothing writes after |\n\n#### Statement parts\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows the line\'s figure carries \u2014 the record\'s own for the earlier rows of the same thing |\n| `same` | alias | yes | A one-link the record and those rows both hold: the rows naming what the record names in it |\n| `at` | alias | no | A moment (a datetime) of those rows no person keys \u2014 when each was certified: with `before`, only the rows whose moment stands before the record\'s, earliest first; absent with it, every row so named, in their own order |\n| `before` | alias | no | A moment (a datetime) of the record no person keys \u2014 when it was made: the moment the rows\' `at` stands before; stated with `at` |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| [comparison](#values) \\| `{ "empty": true \\| false }`) | no | Only the rows whose single select holds one of these options, whose yes/no (stored, or a formula) is this value, whose number or date meets its bounds, or whose field is empty or filled \u2014 read so on the server |\n| `columns` | list of alias (1\u20132) | yes | The rows\' fields read as each one\'s words, in order (its date, its percent) |\n| `figure` | alias | yes | The rows\' number each one reads at the figures\' edge, frozen when the row was certified: a stored number an act fills (`{ "formula": \u2026 }` as it runs) or the server copies when the row is made (`default_from`) \u2014 never one a person keys, nor a formula, rollup or lookup the server recomputes |\n\n<!-- generated:end node-statement -->\n\n```json\n{\n "type": "Statement",\n "key": "claim",\n "lines": [\n { "field": "contract_value" },\n { "field": "done_to_date", "op": "times" },\n { "field": "value_to_date", "op": "equals" },\n { "field": "certified_before", "op": "minus", "parts": { "rows": "claim", "same": "contract", "at": "certified_at", "before": "made_at", "where": { "kind": ["work"] }, "columns": ["claimed_on", "done_to_date"], "figure": "certified_value" } },\n { "field": "claim_value", "op": "equals" },\n { "field": "retention", "op": "minus", "rate": "retention_rate" },\n { "field": "payable", "op": "equals" }\n ],\n "when": { "kind": ["work"] }\n}\n```\n\n## Export node\n\nAn Export is a report file of the rows in view: the title, a line per chip, the search and the moment it was exported, the\nReadings, and every row, at most 20,000. With no props, the one Export is the workbook of the List\'s columns, the Readings\non a sheet before the rows. A List states the reports the business sends \u2014 one a button, several one menu \u2014 each a `label`,\na `description`, and a `template` filled with `title`, `lines`, `readings`, `at` (the moment it was made), `dates` (each\ndate filter\'s `from` and `to` by field) and the rows under the table\'s alias (an html one made a PDF, an excel one a\nworkbook). A report\'s `filename` names its file in the template grammar over the same keys but the rows. Where `per` is a\none-row link, `per_row.<field>` reads a text, number, date or yes/no field of the picked row, and `| add_days:<n>` shifts a\ndate by whole days (`n` a number or such a field, empty for none): `Stock {{per}} {{period.to | add_days:per_row.day_offset | format:"dd.MM.yyyy"}}`. A report of some\nrows (`where`), or of one value the reader picks (`per`: one file per value), is made of the List as those chips would\nnarrow it; the screen keeps the chips and rows it shows.\n\n<!-- generated:start node-export -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | Unique among its siblings; part of the node\'s address |\n| `label` | text | no | The report\'s name; absent on the one Export that stands for the register\'s workbook |\n| `description` | text | no | What the report is for, in a sentence |\n| `template` | alias | no | An html or excel document template the report fills, with `title`, `lines`, `readings`, `at`, `dates`, `period`, `per` and `per_row` (a one-row `per`\'s picked row, the fields `filename` reads); absent, the workbook of the List\'s columns |\n| `filename` | text | no | The file\'s name, at most 500 characters, in the template grammar, over one value each: `title`, `at`, `dates.<field>.from` or `.to`, `period.from` or `.to`, `per`, `per_row.<field>` (a text, number, date or yes/no field of the row a one-row `per` picks), `lines \\| lookup:<n>`; `\\| add_days:<n>` shifts a date by whole days, `n` a number or `per_row.<field>`. Absent, the report\'s label |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `per` | alias | no | A select, member or one-row link the reader picks one value of: one file of that value |\n| `books` | list of alias (at least one) | no | Of the List\'s books, the tables the report reads; absent, all |\n\n<!-- generated:end node-export -->\n\n## Book node\n\nA Book is another table read as part of the register: the same job over its rows \u2014 a legacy ledger, a second branch\'s book.\nA row of the book opens the List\'s Record, lined up by alias; a field the book lacks is blank on its rows and drops from\nwhat they open, and an Act, Check or add needing it is not offered on them. Options line up by alias. The Book\'s own Acts\nand Checks, in its own fields, stand beside the List\'s, each Act\'s `key` its own among the App\'s: an Act of the List running\nits own `workflow`, papers or a call never runs on a book\'s rows, so the Book states its own. With `column: true` the List\ndraws a column naming the table each row is of, and a filter by it. A Book holds the title, the stage, the scope and every\nCheck locking the rows; a table, cards or a plan reads Books.\n\n<!-- generated:start node-book -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | Another table read as part of the register: the same job over its rows |\n| `fields` | map of alias \u2192 alias | no | A field of the List\'s entity \u2192 the field of this entity holding the same fact, where their aliases differ |\n| `column` | `true` | no | The List draws a column naming the table each row is of, and a filter by it |\n| `row` | [Book row](#book-row) | no | The book\'s rows, stated where they differ from the List\'s |\n| `children` | list of ([Record](#record-node) \\| [Act](#act-node) \\| [Check](#check-node)) (at least one) | no | The Fields a save of the book\'s rows writes (and `edit: false` for none), in a Record as a child List\'s Record states them \u2014 the page a row opens is the List\'s; and the book\'s own Acts and Checks over its rows, beside the List\'s its rows can run |\n\n#### Book row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `status` | `{ "edit": true }` | no | `{ edit: true }`: the book\'s stage is also moved by hand |\n\n<!-- generated:end node-book -->\n\n**Writes:** the List\'s `remove`, its Fields and its Acts, on the book\'s own table.\n\n## Query node\n\nA Query is a read for agents, never drawn. An app generates a query for everything its screen reads (`agent_surface` lists them); a Query declares one the screen never asks, called through `run_app_query` or the app\'s API: the\ntable whose rows it reads, the fields each row returns or the figure they add up to, and what narrows them.\n\n<!-- generated:start node-query -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `description` | text | yes | What the query answers, read by whoever calls it by its alias \u2014 at most 300 characters |\n| `params` | map of alias \u2192 [Workflow value](#workflow-value) | no | What a call states, by name, each read by an entry of `where` \u2014 a `number`, `boolean`, `date`, `select`, `record_link`, `member`, holding one. `required: false`: the call may leave it out, and the condition reading it narrows nothing |\n| `rows` | alias | yes | The entity whose rows it reads, under that entity\'s read scope. The app\'s own entity is read as its list reads it: kept by the register\'s `where`, and inside the row the app\'s `scope` names, which the call states |\n| `fields` | list of alias (at least one) | no | The fields each row returns, by alias. A query states `fields` or `reading`, one of the two |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order the rows return in; absent, the order the entity\'s list reads in. Only with `fields` |\n| `limit` | integer | no | The most rows returned. Only with `fields` |\n| `reading` | [Query reading](#query-reading) | no | The rows counted or summed instead of returned: one row for all of them, or one per value of `by` or per day of `over`, each carrying its `by` field or `__day`, `__count`, and `__sum` where `value` is stated |\n| `key` | alias | yes | The query\'s name, what `run_app_query` calls; unique in the app |\n| `where` | map of alias \u2192 [condition](#query-node) | no | What narrows the rows, as a List\'s `where` does: a select\'s options, a yes/no\'s value, a number or date meeting its bounds (`gt`, `gte`, `lt`, `lte`), a field `{ "empty": true \\| false }`, `{ "me": true }` on a member field \u2014 or a value the call states (`{ "param": name }`), or a date or number range of params (`{ "from": \u2026, "to": \u2026 }`) |\n\n#### Query reading\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `average` | `true` | no | The `value` is averaged: summed over the rows holding one, beside how many hold one. Never with `by` or `over` |\n| `by` | alias | no | The single select, one-row link, member or yes/no each row is counted under \u2014 one row returned per value |\n| `over` | alias | no | A date: one row returned per day of it. Never with `by` |\n\n#### Query param\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `param` | text | yes | A param of this query: the condition holds what the call states for it, and is left out where the call states none |\n\n#### Query range\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | [Query param](#query-param) | no | The param the field is on or after \u2014 a date\'s first day, a number\'s least |\n| `to` | [Query param](#query-param) | no | The param the field is on or before \u2014 a date\'s last day, a number\'s most |\n\n<!-- generated:end node-query -->\n\n```json\n{\n "type": "Query",\n "key": "fees_unbilled",\n "description": "A customer\'s fees no invoice covers yet, oldest first.",\n "params": {\n "customer": { "type": "record_link", "entity": "customer", "description": "The customer whose fees are read." },\n "since": { "type": "date", "required": false, "description": "Only fees charged on or after this day." }\n },\n "rows": "fee",\n "fields": ["visit", "service", "amount", "charged_on"],\n "where": { "billed": false, "customer": { "param": "customer" }, "charged_on": { "from": { "param": "since" } } },\n "sort": { "field": "charged_on" },\n "limit": 200\n}\n```\n\nAn entry of `where` holds what a Reading\'s `where` holds, or a value the call states: `{ "param": "<name>" }`. A required\nparam is always stated; one stating `required: false` may be left out, and the condition reading it then narrows nothing.\nA query reads under its table\'s read scope, as every read of the app does. A `reading` returns one row for all the rows it\nreads, or one per value of `by` or per day of `over`. `set_app` compiles a Query with the rest of the app: no code is set.\n\nEvery Reading of the app is a query too, returning its groups, called by its key. A Tab\'s `totals` read is called by\n`<table>_totals_<tab>` with the period\'s two days, `from` and `to`, both required.\n\nA query reads one table: on an app that states `Book`s, whose list reads several, it names a book\'s own table, never the\nList\'s. A param compares a field holding one value \u2014 or, as a range, a date or a number \u2014 and never a number read in each\nrow\'s own unit or currency, nor a date holding a span.\n\n## Workflow node\n\nA Workflow is a verb for agents, never drawn: a job no press of the screen starts \u2014 an agent or a service handing the app a\nfile, or a list of rows to reconcile a table against. It states what it takes and what it hands back; a table is named by\nits alias in `tables` and a select by `<table>.<field>`. It is called through the app, by `run_app_workflow` or the app\'s\nAPI. On an app shared by a public link it runs only for someone who holds the app, and a visitor\'s call is refused as an\nalias the app does not have.\n\n<!-- generated:start node-workflow -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `description` | text | yes | What the workflow does, read by whoever calls it by its alias \u2014 at most 300 characters |\n| `inputs` | map of text \u2192 [Workflow value](#workflow-value) | no | What a call hands it, by name \u2014 checked before its body runs |\n| `outputs` | map of text \u2192 [Workflow value](#workflow-value) | no | What its body returns as `data`, by name \u2014 checked as it returns; absent, read off the body |\n| `key` | alias | yes | The workflow\'s name, what `run_app_workflow` calls; unique in the app |\n\n#### Workflow value\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"boolean"` \\| `"date"` \\| `"datetime"` \\| `"email"` \\| `"json"` \\| `"record_link"` \\| `"select"` \\| `"member"` \\| `"file"` \\| `"object"` \\| `"array"` | yes | What the value is |\n| `description` | text | no | What the value is for, read by whoever calls the workflow |\n| `required` | `false` | no | false: the value may be left out; absent, it is always there |\n| `multi` | `true` | no | A `record_link`, `select`, `member` or `file` holding several |\n| `entity` | alias | no | The entity a `record_link` names a row of |\n| `field` | text | no | `<entity>.<field>`: the select field whose options a `select` takes, as the field holds them when the workflow runs |\n| `options` | list of alias (at least one) | no | The values a `select` takes, where no field holds them |\n| `fields` | map of text \u2192 [Workflow value](#workflow-value) | no | An `object`\'s values, by name |\n| `items` | [Workflow value](#workflow-value) | no | What each item of an `array` is |\n\n<!-- generated:end node-workflow -->\n\n```json\n{\n "type": "Workflow",\n "key": "reconcile_fees",\n "description": "Reconciles a visit\'s fees against the lines of the carrier\'s statement.",\n "inputs": {\n "visit": { "type": "record_link", "entity": "visit" },\n "statement": { "type": "file", "required": false },\n "lines": { "type": "array", "items": { "type": "object", "fields": { "service": { "type": "select", "field": "fee.service" }, "amount": { "type": "number" } } } }\n },\n "outputs": { "added": { "type": "number" }, "changed": { "type": "number" } }\n}\n```\n\nIts code is set on the app, never stated in the tree: `set_app_workflow` with the app, the key and the `source`, leaving\n`inputs` and `outputs` out \u2014 the code is checked against what the node declares. Every `set_app` keeps that code and holds\nwhat it takes, what it hands back and its description to the node. To change what a workflow takes or hands back, change\nthe node first: `set_app` keeps the code where it still meets the change, and where it does not the workflow stands on no\ncode again until `set_app_workflow` sets one that does. With no code it refuses every call, and each `set_app` says so. A\nWorkflow the tree stops declaring is archived with its key.\n\n## Composed page\n\nA Record whose page is its own arrangement of what the app already states \u2014 a piece of writing under its title, a brief\nbeside its figures \u2014 states `compose` instead of parts: a tree of finished pieces, each named by `use`. A `Rows` holds\nthe Acts and Checks of its rows as `children`, each named by `type`: they are the app\'s own acts and checks on those rows,\nnever pieces of the page.\n\n```json\n{\n "type": "Record",\n "door": "page",\n "compose": {\n "use": "Column",\n "children": [\n { "use": "Bar", "children": [{ "use": "Status" }, { "use": "Field", "field": "audience" }, { "use": "Act", "act": "send_for_review" }, { "use": "Copy", "field": "body", "as": "plain" }] },\n { "use": "Title" },\n { "use": "Document", "field": "body" },\n { "use": "Group", "title": "Reviews", "children": [{ "use": "Rows", "child": "review", "create": false, "children": [{ "type": "Act", "key": "resolve_review", "label": "Resolve", "workflow": "resolve_review", "confirm": true }] }, { "use": "Act", "act": "ask_reviewer" }] }\n ]\n }\n}\n```\n\nA node binds by name: a `field` is a field of the record\'s table, an `act` an Act of the app, a `child` a table linking to\nthe record. What a name binds is read and written exactly as a part naming it would be \u2014 a `Field` and a `Document` are\nsaved through the record\'s own save, an `Act` runs its workflow, a `Rows` reads the child\'s rows \u2014 so a piece adds no write\nof its own; only an Act inside a `Rows` declares one, as an Act of a child List does. A piece holds no condition and no computed value: a piece that does not apply on a row draws nothing, and anything\nthat must depend on the row is an Act\'s `when` or a field\'s formula, never the tree.\n\nThe first `Act` the page itself draws is its primary and every other one secondary; a `danger` act reads so wherever it\nstands. A `Copy` and a `Document` naming one field copy what is on screen \u2014 the words typed and not yet saved among them. The\nrecord\'s chrome stays the runtime\'s: the way back, its comments door and its \u22EF with Delete share the row over the tree, and the\nhead draws no title and no status of its own \u2014 the tree\'s `Title` and `Status` do.\n\n<!-- generated:start apps-compose -->\n\n#### Column piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Column"` | yes | The page\'s reading column, 680px: its pieces stacked in order from one left edge |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces stacked in it, in the order they are read |\n\n#### Bar piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Bar"` | yes | One row of small things on one control height, in two zones: what the record is \u2014 a status, a field\'s control \u2014 from its left edge, what is done to it \u2014 an act, a copy \u2014 ending on its right edge, taking a row of their own under it where the row runs out of room |\n| `children` | list of [piece](#composed-page) (at least one) | yes | Its pieces, each zone\'s in order; a `Field` directly in it is its control alone on one line, named by its label where it holds nothing; an `Act` that destroys or corrects is not drawn here \u2014 it stands in the record\'s \u22EF |\n\n#### Group piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Group"` | yes | A titled stack, headed once by `title` |\n| `title` | text | yes | Its heading, in the reader\'s words; holding one `Rows`, it carries their count and their add, and the group\'s `Act`s end its line |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces under the heading, in order |\n\n#### Panel piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Panel"` | yes | What a reader does not need while writing: nothing in the page, one entry of the record\'s \u22EF named by `title` that opens its pieces in a panel beside the page \u2014 a sheet on a phone; directly in the root `Column` alone |\n| `title` | text | yes | Its entry in the \u22EF and the panel\'s name, in the reader\'s words; an `Act` directly in it ends that name\'s row |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces in the panel, in order: `Rows`, `Field`, `Act`, `Group`, `Document` \u2014 never `Title`, `Status`, `Bar`, `Copy` or another `Panel` |\n\n#### Title piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Title"` | yes | The record\'s title (`records.<entity>.title`) as the page\'s heading, written in place where a person writes it; one a page |\n\n#### Status piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Status"` | yes | The record\'s status (`records.<entity>.status`) as a record\'s head draws it: moved by hand, by its acts\' moves, or read |\n\n#### Field piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Field"` | yes | A field of the record under its label, as a section\'s `fields` draws it |\n| `field` | alias | yes | The field drawn \u2014 never the title or the status, which `Title` and `Status` draw; once a page |\n\n#### Document piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Document"` | yes | A markdown text field written in place with no box, saved as it is written \u2014 whether it is saved said at its foot |\n| `field` | alias | yes | A text field with `format: "markdown"`; once a page |\n\n#### Act piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Act"` | yes | An act of the app on the record as its button: its `when`, what it lacks, its asks and its confirmation as everywhere; the first `Act` the page itself draws is its primary |\n| `act` | alias | yes | An act of this app on the record \u2014 never one `of` a child\'s rows, which stands on its row; once a page |\n\n#### Rows piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Rows"` | yes | A child entity\'s rows under the record, as a rows block lists them, each row\'s own acts on it |\n| `child` | alias | yes | A child entity linking to the record; once a page |\n| `create` | `false` | no | false, rows are not added here \u2014 an act of the page makes them |\n| `remove` | `true` | no | The rows offer a Delete (open row and \u22EF); absent, none |\n| `children` | list of ([Act](#act-node) \\| [Check](#check-node)) (at least one) | no | The acts and checks of its rows, each standing on every row listed, as a child List\'s stand on its rows; absent, the rows offer none |\n\n#### Copy piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Copy"` | yes | A button copying a field\'s words as the page holds them now \u2014 what is typed in its `Document` and not yet saved among them |\n| `field` | alias | yes | The text field copied |\n| `as` | `"plain"` \\| `"raw"` | yes | `plain`, its markdown marks gone; `raw`, as stored |\n\n<!-- generated:end apps-compose -->\n\n## Tasks\n\nA table whose `records` states `"task": true` \u2014 its status a stored select with `closed` \u2014 is work someone finishes.\nEvery List of its rows draws it alike, each row led by a ring. Ticking the ring moves the row to a closed option by the\none write the app already has for that move: the Act of that table setting a closed option, offered on an open row and\nasking nothing, else its own save where people write the status \u2014 stamping what that act stamps and appending the\nstatus history. Unticking runs the Act moving a closed row back, else the save back to the status\'s default. A move no\nwrite reaches leaves the ring disabled and the check notes it. An Act with `on: "ticked"` completes several at once.\n\nA record listing task rows filed under it by a link holding one row says how much of that work is done \u2014 those at a\nclosed option of all \u2014 wherever its row is drawn: a meter on its head, a column of its register\'s rows, a card\'s foot, a\ncalendar entry, a gantt\'s or a lane\'s bar. No rollup is stated for it.\n\n```json\n{\n "tables": [\n {\n "ref": "project",\n "label": "Projects",\n "singular": "Project",\n "fields": {\n "name": { "label": "Name", "required": true, "type": "text" },\n "lead": { "label": "Lead", "type": "member" }\n },\n "title": "name",\n "records": {}\n },\n {\n "ref": "task",\n "label": "Tasks",\n "singular": "Task",\n "fields": {\n "title": { "label": "Title", "required": true, "type": "text" },\n "project": { "label": "Project", "required": true, "type": "link", "table": "project", "multi": false },\n "assignee": { "label": "Assignee", "type": "member" },\n "status": {\n "label": "Status",\n "required": true,\n "default": ["todo"],\n "type": "select",\n "options": [\n { "alias": "todo", "label": "To do", "color": "slate" },\n { "alias": "done", "label": "Done", "color": "green" }\n ]\n },\n "due": { "label": "Due", "type": "date", "format": "date" },\n "done_on": { "label": "Done on", "type": "date", "format": "date" }\n },\n "title": "title",\n "records": {\n "subtitle": ["assignee"],\n "due": ["due"],\n "task": true,\n "starts": { "assignee": "me" },\n "status": { "field": "status", "closed": ["done"] }\n }\n }\n ]\n}\n```\n\n```json\n{\n "body": {\n "type": "App",\n "key": "projects",\n "name": "Projects",\n "color": "blue",\n "tables": { "project": "<id of the project table>", "task": "<id of the task table>" },\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "project",\n "children": [\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "work",\n "title": "Work",\n "children": [\n "lead",\n {\n "type": "List",\n "entity": "task",\n "columns": ["due"],\n "children": [\n { "type": "Act", "key": "complete", "label": "Complete", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": { "now": true } } },\n { "type": "Act", "key": "reopen", "label": "Reopen", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } },\n { "type": "Act", "key": "complete_all", "label": "Complete", "on": "ticked", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": { "now": true } } }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\nThe tasks as a register of their own, for one person: a one-part Record holds its Fields and Acts directly.\n\n```json\n{\n "body": {\n "type": "App",\n "key": "my_tasks",\n "name": "My tasks",\n "color": "teal",\n "tables": { "project": "<id of the project table>", "task": "<id of the task table>" },\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "task",\n "opens": { "assignee": { "me": true } },\n "children": [\n { "type": "Reading", "key": "open_tasks", "label": "Open", "where": { "status": ["todo"] } },\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n "project",\n "due",\n { "type": "Act", "key": "finish", "label": "Finish", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": { "now": true } } },\n { "type": "Act", "key": "undo", "label": "Undo", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## App rules\n\nEvery rule the check enforces over an app, one sentence each under an id; a finding names its rule\'s id\n(`[register.filter]`), and `model/<rule id>` is that one rule\'s page. A sentence names the node and the prop an author writes (`List.create`, a `Section`);\neach area stands in the tree where this table says:\n\n| Area | Stands at |\n|---|---|\n| `app` | the App, and the register List\'s props that belong to the app: `words` (labels), `color`, `scope`, Books, logs |\n| `register` | the register List: its columns, filters, search, group, sort, layout, tabs, period, export |\n| `reading` | a Reading or Progress |\n| `record` | a Record and its parts: Section, Side, Page, Act placement |\n| `block` | what a part holds: a child List, a Field gallery or prose block, a Reading, a desk, a timeline or agenda |\n| `statement` | a Statement |\n| `act` | an Act |\n| `check` | a Check |\n\n<!-- generated:start rules-app -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.alias-unique` | An App\'s `key` is unique among apps, and an Act\'s among the Acts of its App, every List\'s included. |\n| `app.icon` | An App\'s `icon` is a Lucide icon name in kebab-case, as `receipt` or `trending-up` are. |\n| `app.add-label` | A List\'s `add_label` stands only where it adds rows (`create`), in words that are not empty and hold no `{` or `}`. |\n| `app.labels` | An App\'s `words` name a table it reads and a field or option of it, in words that are not empty and hold no `{` or `}`; no two fields of a table, nor two options of a field, show the same words. |\n| `app.records-entry` | Every table an App draws a List over, opens or picks from \u2014 the register\'s, a child List\'s, a linked row\'s, a documents List\'s \u2014 has a `records` entry. |\n| `app.scope` | The App\'s `scope` is a required one-row link of the first List\'s entity, the same on every row the App lists \u2014 never a `columns`, `filters` or `search` entry, `group`, `where`, `opens`, a Field of a part or a `create` question. |\n| `app.when` | A node\'s `when` holds single selects to options, yes/nos to `true` or `false` (stored or a formula of one; a formula with `options` is a select), numbers and dates to bounds (`gt`, `gte`, `lt`, `lte`; a date\'s bound is a day, a moment on a datetime, or `{ "now": true }`) and any field but a yes/no to `{ "empty": true \\| false }` \u2014 a number read in each row\'s own unit or currency and a lookup take no bound. |\n| `app.asked-written` | What a List\'s `create` asks, what an Act\'s `asks` asks and what `starts` fills is a field a person writes \u2014 never a formula, rollup, lookup, autonumber or a date the workspace stamps. |\n| `app.own-rows` | A many-link to rows that each belong to one row of this entity is never a Field of a part, a `create` question or an Act\'s `asks`: those rows are the record\'s own, read as a child List. |\n| `app.create-asks` | A List\'s `create` asks fields a person writes \u2014 never the link or the folder it files the row under, which the add sets itself. |\n| `app.create-required` | A List\'s `create` asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add. |\n| `app.books` | A register App\'s Books each name another entity once (`entity`) \u2014 never the App\'s own \u2014 whose fields line up with the register List\'s by alias, else by `fields` (a field the List reads, or one an Export prints \u2192 one of the Book\'s): read as one column type (a day apart from a moment), one value or several alike, a link to the same entity. Each Book holds the title, the status where the List shows one, the folder (`scope`), what `where` keeps and every Check locking the rows; the List is a table, cards or a plan, whose `at`, `across` and `up` line up like any field. Two Books holding a field of their own under one alias hold it as one kind of value. An Export\'s `books` each name the List\'s entity or one of its Books, once, and one of them holds each field its `where` and `per` narrow by. A Book\'s `column: true` draws the column naming each row\'s table in a List that has Books. A Book\'s own Acts and Checks, over its rows in its own fields, stand beside the List\'s: each Act\'s `key` its own among the App\'s Acts, its Books\' and its logs\', and its `workflow` run by no Act of another List or Book. |\n| `app.book-option-kin` | *Noted, never refused:* A Book\'s option labelled as one of the App\'s, under another alias, reads apart from it: the List lists both and counts each on its own. |\n| `app.book-lacks` | *Noted, never refused:* A Book lacking a field the Record, an Act or a `create` reads: what reads it drops from the Book\'s rows \u2014 an Act, a child List, a Section, the `create`. An Act of the List running its own `workflow`, reading papers or recording a call, and an Act of a child the Book\'s record lists no rows of: none is offered on the Book\'s rows, which run the Book\'s own Acts. A field only an Export prints, held by a Book as another kind of value, prints blank on its rows. A Tab\'s `totals` over a child linking to no row of a Book total nothing on its rows. |\n| `app.logs` | A register App\'s logs \u2014 further Lists, at most 3 \u2014 each list another entity under a `key` of their own: never the App\'s own entity, one of its Books\' or another log\'s. A log is one List \u2014 a table or cards, with no Tabs, no Book `column` and no composed Record \u2014 over an entity whose status states no owned `stages`, and stands in an App that states no `scope`. An Act\'s `key` is its own among the App\'s Acts, its logs\' included. No Record of the App or of a log lists the rows of an entity another of the App\'s Lists is over: a link to one opens it on its own List. A List people work as a desk of its own is another App. |\n| `app.row` | A List\'s `row` states only what differs from `records`: a `subtitle` that is not the title, a number for its `figure`, dates for its `due`, an `image` that is a files picture no Act makes documents into, or `false` for a figure, deadlines or picture the entity states and the App draws none of. |\n| `app.tasks` | *Noted, never refused:* A task entity an App lists whose rows no write of the App moves to a closed option (its ring stands disabled), or that an Act ticks done and none moves back. |\n| `app.workflow-value` | A value a Workflow takes or hands back states what its type needs and nothing another type does: a `record_link` its `entity`, a `select` its `options` or the `field` holding them \u2014 never both \u2014 an `object` its `fields`, an `array` its `items`; `multi` stands on a `record_link`, `select`, `member` or `file`; an output is never a `member` or a `file`; and a value nests at most 8 deep. |\n| `app.query` | A Query reads one entity\'s rows as `fields` or as a `reading`, one of the two \u2014 never the App\'s own entity where the App has Books, whose List reads several tables; `sort` and `limit` stand with `fields`; a `reading` splits by `by` or by the day of `over`, never both, and averages only with neither; each param is a number, a yes/no, a date, a select, a one-row link or a member holding one, read by an entry of `where` on a field of its type \u2014 a date by a range, a number by either, and never a number read in each row\'s own unit or currency or a date holding a span. |\n| `app.generated-name` | A name the App generates is taken by nothing the App states. No Query, no Reading\'s or Progress\'s `key` \u2014 a log\'s as it is read, `<entity>_<key>` \u2014 and no Tab\'s read (`<entity>_list_<tab>`) is the name of a read the App generates, of a Query or of a Reading. |\n| `app.surface-described` | *Noted, never refused:* A value a Workflow takes, and a param a Query takes, that states no `description`: the one line a caller reads to fill it. |\n| `app.keys-unique` | No two children of one node share a key: an address names one node. |\n| `app.states-list` | A register App states the List whose rows it lists; an App stating none is a dashboard and holds Readings. |\n| `app.period` | An App\'s `period` is a dashboard\'s: a register states its own on its List. |\n| `app.dashboard-words` | A dashboard reads every table\'s rows whole and states no `words` or `scope`: those are a register App\'s. |\n| `app.add-once` | A table\'s add by a picker is stated once \u2014 at one Field\'s `add` or one link input\'s `add` \u2014 and every picker of that table adds by it. |\n| `app.card-once` | A table\'s card (`record`, `remove` on a link Field) is stated once, at one Field. |\n| `app.card-fields` | A card\'s `record` names, once each, fields of the linked table its save may change. |\n| `app.picker-link` | `add`, `record` and `remove` stand on a Field that is a link: only a link has a picker and a card. |\n| `app.add-filed` | The Lists adding rows of one table file them by one link, the first such List\'s: a List over that table filed by another link lists its rows and adds none. |\n| `app.default-breaks` | A node the platform draws where the tree states none leaves the App refusing nothing more: where it would, the tree states that node itself. |\n| `app.adds-field` | A picker\'s `add` names fields of its table a person writes, each once. |\n| `app.picks-only-field` | `add: false` stands at a link, once, to an entity whose add the app states. |\n\n<!-- generated:end rules-app -->\n\n## Reading rules\n\n<!-- generated:start rules-reading -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.value` | A Reading sums a number, never a percent \u2014 percents do not add up; a value of `{ avg }` reads one. |\n| `reading.average` | `{ avg }` averages a Reading\'s `value` over the rows holding one, read against a plain `target` \u2014 never a value `limits` bounds, whose bound is a sum. |\n| `reading.split` | `by` and `across` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 or by the bands `tiers` cut a number read in one unit on every row; a date `by` splits them per period, as a trend; a grid crosses two different fields. |\n| `reading.over` | `over` is a date: the row\'s own, stored or a formula, or its parent\'s through a lookup over a one-row link. |\n| `reading.where` | A reading\'s and a Query\'s `where` keep rows by a select\'s options, a yes/no\'s `true` or `false`, a number or date\'s bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }`), `{ "empty": true \\| false }`, or a member field holding the reader (`{ "me": true }`); a List\'s `where` also keeps rows by `<link>.<field>`, a select or yes/no of the row a one-row link names; a Query\'s `where` also holds a field to what the call states (`param`, `from`, `to`). |\n| `reading.target` | `target` reads a value nothing bounds \u2014 a value `limits` bounds reads against the sum of its bound. |\n| `reading.better` | `better` needs `over`: a change is read over the periods its date places the rows in. |\n| `reading.list-app` | A List\'s `app` is the id of an app whose first List is over the List\'s own entity. |\n| `reading.progress` | A Progress reads dates of its rows for `planned` and `finished`, a number for `done` and `weight`, and rows `measured` through a one-row link to them, by a date of theirs and a number of theirs. |\n| `reading.alias` | A Reading\'s or Progress\'s `key` is its own among the App\'s Readings and Queries, a log\'s read by its entity before it (`<entity>_<key>`); a Progress above a List reads the List\'s own rows and states no name of its own: its `key` is the one its place gives it, `register_reading_<position>`. |\n| `reading.tabs` | A dashboard with Tabs holds every Reading and List inside a Tab, and every Tab holds one at least; without Tabs, none sits in one. |\n| `reading.restate` | A dashboard\'s count tile never counts the rows a List beside it lists under the same `where` and tab: the List\'s head counts them. |\n| `reading.period` | A dashboard\'s `period` is the one it opens on, and it stands only over a Reading placed in time by `over`. |\n| `reading.funnel-place` | A Reading with `steps` (a funnel) stands in a part of a Record: a band and a dashboard draw none. |\n| `reading.on-hand-split` | A Reading with `on_hand` compares what is owed with what is on hand per value of one field, or two: its `by` and `across` name fields, never bands. |\n| `reading.average-single` | An average (`value: { avg }`) is a single figure\'s: its Reading states no `on_hand`, `by` or `across`. |\n| `reading.grid-rows` | `across` splits a grid\'s columns, and stands only beside the `by` splitting its rows. |\n| `reading.tab-holds` | A register\'s Tab narrows its rows and holds no nodes: Readings stand beside it in the List. |\n| `reading.band-off` | `readings: false` turns off the band a register leads with by default: a List stating it states no Readings or Progress, which are the band. |\n\n<!-- generated:end rules-reading -->\n\n## Statement rules\n\n<!-- generated:start rules-statement -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `statement.lines` | A Statement\'s `lines` are numbers of the record, each once \u2014 the first stating no `op`, every other one, the last an `equals` \u2014 each `equals` a formula, rollup or lookup, and a `rate` a number of the record. |\n| `statement.rows` | A line\'s `parts` read rows of an entity holding `same`, a one-link of the record\'s to the same entity; `at`, a moment of those rows, and `before`, a moment of the record (datetimes, never days), stated together; their `columns` and `figure` are those rows\' fields, the figure a number. |\n| `statement.parts-keyed` | A line\'s `parts` add up to its figure, so no person keys it: a formula, rollup or lookup, a value the server copies when the record is made (`default_from`) that no `create` asks and no save or Act writes, or a stored number the App\'s saves leave out \u2014 the line draws it read; each part\'s `figure` is frozen when its row was certified \u2014 a stored number an Act fills, the server copies or the App\'s saves leave out, never one a person keys or the server recomputes \u2014 and no person keys the moments `at` and `before` either. |\n\n<!-- generated:end rules-statement -->\n\n## Check rules\n\n<!-- generated:start rules-check -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `check.field` | A Check\'s `field` is a yes/no or text formula of the rows of the List it stands in. |\n| `check.of` | A Check stands in the register List or in a child List the Record lists. |\n| `check.not-placed` | A Check\'s field is never also placed on the record as a Field \u2014 the line under the head says it. |\n| `check.blocks` | A Check\'s `on` names what it refuses: `"update"` (the saves), an Act\'s `key`, or `"all"` (every Act and save); a child List\'s Check refuses that row\'s saves and the Acts of it, never the record\'s. It binds this App alone. `"delete"` refuses this App\'s removal of the rows and is kept for a rule no check of the table states: a Delete is the table\'s checks\' to refuse. |\n| `check.resolve` | A Check\'s `resolve` names an Act of this App on the rows the Check stands on, pressed on one row, not refused by the Check, and drawn as a button \u2014 never a correction. |\n| `check.restates-meter` | A warning reading a gated meter\'s figure against its bound or pass mark is refused \u2014 the meter draws it. |\n\n<!-- generated:end rules-check -->\n\n## Register rules\n\nThe rules of the register List\'s props: columns, filters, search, group, sort, layout, tabs, period and export.\n\n<!-- generated:start rules-register -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.export-report` | A List\'s Exports each have their own `label`; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes. |\n| `register.export-template` | A template an Export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the List\'s entity alias \u2014 an entity named none of those \u2014 and is the document of no Act. |\n| `register.column-drawn` | A List\'s `columns` never name what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 and name a field once. |\n| `register.columns-budget` | A List\'s `columns`, each at the width of its kind, fit the room a desk leaves beside its name and the row\'s own status, deadline and figure. |\n| `register.filter` | A List\'s `filters` name a select, a member, a link, a date, a yes/no or a number of the List\'s own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes. |\n| `register.filter-split` | *Noted, never refused:* A `filters` entry on a field a Reading of the List with `by` or `across` splits the rows by \u2014 a press on its part narrows by it too; a member\'s filter, which narrows to the reader\'s own rows, is never noted. |\n| `register.filter-unshown` | A `filters` entry, a narrowing the List `opens` on, and a press in its band (a Reading\'s part, a grid\'s cell, a figure\'s `where`) name a field its rows show \u2014 a `columns` entry; the row\'s title, the line under it, its picture, status or figure, a meter\'s bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under \u2014 or a formula, drawn where every field of its own row it reads is (through other formulas too; one reading no field is drawn nowhere). A narrowing to the reader (`{ "me": true }`) needs none. A tree leaves out a `filters` entry no row shows and says so (name the field as a column or in the row\'s line to offer it); the other two are refused. |\n| `register.tiers` | `tiers` band a number read in one unit on every row; a figure read in each row\'s own unit or currency is named bare. |\n| `register.search` | A List\'s `search` names text, a code, or a link \u2014 matched by its row\'s title. |\n| `register.sort` | Each `sort` key of a List or a Tab names a field of the List\'s rows that holds an order \u2014 never files \u2014 and no field twice. |\n| `register.tabs` | A List\'s Tabs each have a `key` of their own, none the entity\'s alias or one a report template holds (`title`, `lines`, `readings`, `at`, `dates`, `period`, `per`); a Tab states one of `dated` and `open` (dates of its rows, `open`\'s two different) or `totals` (a child\'s rows windowed by the period); its `where` and `sort` hold as the List\'s do, and never narrow by the status, which its chips split. Only a table or cards List has Tabs, and a Reading\'s `tab` names one of them. |\n| `register.tab-totals` | A Tab\'s `totals` name a child linked to the List\'s entity by a one-row link (a child linking the List\'s entity twice cannot be totalled), a date of the child, never a moment or a span, and 1 to 6 columns each under its own alias and label; a column\'s `value` is a number of the child, its `where` holds over the child\'s selects and yes/nos, and a value read in each row\'s own unit or currency stands only where that select of the child looks up, through its link, a single select of the List\'s entity, and the child links to no Book\'s rows. A Book\'s rows are totalled by the child\'s one link to them, by one field to one row; a Book the child links to not at all totals nothing on its rows (`app.book-lacks`). Only a table List has them, and no Tab states `totals` beside `dated` or `open`. |\n| `register.group` | A List\'s `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 the status too, but never on a task list\'s block, whose chips narrow it; only a table or cards List groups, and a gantt into its lanes; grouping by the title needs a `row.subtitle` to lead each row. |\n| `register.readings-own` | A Reading in a List reads the List\'s own entity; another entity\'s stands on a dashboard. |\n| `register.readings-bands` | A Reading in a List that bands a number by `tiers` has that field with the same tiers among the List\'s `filters`: a press on a band narrows the rows by that filter. |\n| `register.readings-restate` | A Reading with no `by` in a List never sums the row\'s figure, counts the rows, or counts one stage\'s rows, unless it is placed in time (`over`): the summary line totals the figure and the status chips count the rows, but neither draws a change against the last period. |\n| `register.where-opens` | A List\'s `where` holds its own selects, yes/nos, bounds and blanks (a child List\'s or the register\'s), and `opens` its selects and yes/nos and a member field holding the reader (`{ "me": true }`) \u2014 or, on a status whose `stages` have owners, the rows waiting on the reader; a field `where` fixes to options is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. A List laid out as a `plan` keeps its items by options and yes/nos alone. |\n| `register.roster` | A List with `layout: "roster"` holds the List of its cells: the cells\' entity has one single link to this one and a date on its line (a field of the row itself, or a lookup of its parent\'s date), a roster draws no `columns`, and an `expect` (with `of`) stands only on the cells List. |\n| `register.roster-expect` | A roster cells List\'s `expect` is a single select of its rows, and its `of` this entity\'s multi-select whose every option is one of `expect`\'s. |\n| `register.gantt` | `layout: { gantt }`: `start` and `end` (absent, the entity\'s first `due`) are two different dates of the row, both days or both moments; each diamond (`milestones`) another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity. |\n| `register.lanes` | `layout: { lanes }`: `by` is a one-row link to an entity with a `records` entry, the rows name a date on their line (a field of the row itself, or a lookup of its parent\'s date), a lane block\'s two ends are read alike (two days or two moments), and `loads` stands with it. |\n| `register.plan` | `layout: { plan }`: `at` is a one-row link to an entity whose `records` states a `plan_place`, `across` and `up` two different numbers of the row, `fill` a single select; a plan List has no Tabs, `group` or `attention`. A write that sets `at`, `across` or `up` is refused unless it clears all three, or places the row at a free cell of a bay (a row from 1 to the bay\'s `across`, a tier from 1 to its `up`, resting on a present item), or at an area with neither. |\n| `register.plan-unique` | *Noted, never refused:* A plan\'s items (the List\'s table and each Book) state `unique` on their place link, `across` and `up`: the app\'s moves refuse a taken cell, and the unique makes every other write hold it \u2014 the note names a table that states none. |\n| `register.plan-mark` | A plan\'s `mark` names a single select of the row and an option of it by alias: a row holding that option says its label on its box and is hatched in the option\'s colour, which the legend names by its label. A Book that holds the select must hold it as a single select with that option; a Book lacking it never marks its rows (`app.book-lacks`). |\n| `register.plan-size` | A plan\'s `size` names a single select of the row and, by option alias, the box (`across`, `along`, `up` \u2014 positive, in the one length unit the places\' `size` states) of each option: a row holding one is drawn as that box, any other at its place\'s cell. A Book that holds the select must hold it as a single select; a Book lacking it draws its rows at their places\' cells (`app.book-lacks`). |\n| `register.plan-box-size` | *Noted, never refused:* A plan whose places state a `size` and whose rows\' select options are ISO 6346 container size-type codes (`20DC`, `40HC`) states the box of each in `size`, from the ISO 668 dimensions, else every box is drawn at its place\'s cell. |\n| `register.loads` | A lanes layout\'s `loads` names one to 2 numbers a lane block adds \u2014 never a percent \u2014 each against a number of the lane\'s row, in one unit or two of one dimension. |\n| `register.band-pivot` | *Noted, never refused:* A Reading with `across` (a grid) among a List\'s Readings: its grid reads on a dashboard, and the band leads with a figure (a Reading with no `by`). |\n| `register.headline` | *Noted by the designer on a write, never refused:* A List over rows that fall due or carry money and have no status whose steps the chips count, laid out as a table, cards or a gantt, has Readings \u2014 they split the rows by a field they show. |\n| `register.span` | *Noted, never refused:* Rows that each span a run of days or hours \u2014 two dates on their line, a `frees`, a `no_overlap` booking them on what its `by` links to, or one required date and the table\'s `due` where the line names no date \u2014 are drawn in time by some List over them (a `layout` of calendar, lanes, gantt or roster) \u2014 the note names the app to add over them, laid out as lanes on what each one books, else as a gantt (a run held by a date and the `due` also asks for both dates on the line). |\n| `register.where-bound` | A bound read from the record (`{ "record": \u2026 }`) in `where` narrows only a List that reads its rows \u2014 `same`, `app`, or a Record with `edit: false`. |\n| `register.where-reader` | A List\'s `where` keeps its rows for every reader and never names the reader (`{ "me": true }`): `opens` opens a List on the reader. |\n| `register.props` | A node states only the props of its place: a List those of the register, a log, a roster\'s cells, a rows, agenda, timeline or read-only List of a record, a document or a dashboard; the Record of a List below the register `edit` and its children; a dashboard\'s Tab its `key`, `label` and children. |\n| `register.holds` | A List holds what its place draws: the register its Tabs, Readings, Progress, Exports, Acts, Checks, Books, Record and, under a roster, the one List of its cells; a child List its Acts, Checks, Record and, with `under`, the List of its documents; a document and a roster\'s cells their Acts, Checks and Record. |\n| `register.layout-place` | The register lays out as a table, cards, a calendar, a roster, a gantt, lanes or a plan; a child List writing its rows as a table, a checklist or a desk; any child List as an agenda or a timeline. |\n| `register.row-restates` | A child List and a log read their rows as `records` states them: their `row` states a picker\'s add or a card on the title or a subtitle, never another `subtitle`, a `figure`, `due`, `image` or `status: false`. |\n| `register.edit-column` | An edit column (`{ "field": \u2026, "edit": true }`) stands in a child List, which writes its rows in place: the register\'s rows are written where they open. |\n| `register.remove-roster` | A roster cells List\'s `remove: true` offers the Delete of the roster\'s rows, so it stands only beside a roster over rows the App writes. |\n| `register.attention` | `attention: false` keeps a table\'s or cards\' rows in the List\'s own order \u2014 a first-come queue; a calendar, lanes, a gantt and a roster stand their rows in time and lift none. |\n| `register.unworked-stage` | *Noted, never refused:* A register lists rows (every open one, else those its `where` keeps) at a stage that no Act of the App is offered at and no Section stands at, though the App states the stages it works: the rows only sit in its list, so keep them out with `where` or leave them to the App that works them. |\n\n<!-- generated:end rules-register -->\n\n## Block rules\n\nThe rules of what a part holds: a child List, a Field gallery or prose block, a Reading, a desk, a timeline or an agenda.\n\n<!-- generated:start rules-block -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.child-link` | A child List reads a child linked to the record; `via` names the link where the child links more than once. |\n| `block.row-blocks` | The Record of a List below the register \u2014 a rows List\'s, a document\'s under its lines, a roster\'s cells\' \u2014 holds its Fields and the Lists of its rows\' own children, each a child List as one level up over an entity linking to that List\'s, checked against that List\'s entity; nothing else. A List below a row states no `under`, `pick`, `expect` or desk, and a List stating `expect`, `under` or a desk holds no List below its rows. |\n| `block.column-drawn` | A child List\'s `columns` never name what the child\'s row draws (title, subtitle, image, status, figure, or the bound its figure\'s meter reads against) nor its link back, and name a field once; an `expect` List\'s figure is its filled-in-place column. |\n| `block.columns-budget` | A child List\'s `columns`, each at the width of its kind, fit the room a desk leaves beside its name and the row\'s own status, deadline and figure. |\n| `block.title-back` | Where a child\'s title is its link back to the record a child List lists it under, its first other `row.subtitle` names it there \u2014 a text, a code, a select, a link or a member, never a number or a date. |\n| `block.where` | A child List\'s `where` names single selects the child requires, by options they hold \u2014 one held at a single option is never also a column \u2014 and the child\'s yes/nos by value, numbers and dates by bounds, and fields by `{ "empty": true \\| false }`. A yes/no, a bound or a blank is never held: a child List adds rows only at a stored yes/no\'s start, adds none under a bound or a blank, and never picks, expects or stands lines under documents. |\n| `block.under` | `under` names the child\'s one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines\' columns never read the document (its heading does) nor hold long text. |\n| `block.under-once` | Documents drawn over their lines (`under`) are never listed again as a List of their own. |\n| `block.edits` | A child List\'s editable columns (`{ "field", "edit": true }`) name at most two of the child\'s columns or its figure that the App\'s saves of the child write \u2014 a number, a date, a few words, one option or a one-row link, never a files or long-text field, the List\'s `via` nor a link it must name alike (a link\'s `same`) \u2014 never on an `expect` List. |\n| `block.remove-documents` | The documents List inside a child List with `under` states `remove: true` only over documents the App writes. |\n| `block.compare` | A child List\'s `compare` names two different fields of the child, `before` and `after`: two files fields, or two values of one type (text but long text, a number, a date, a single select). Never with `expect`. Over files on a checklist layout, `before` is the check\'s `photo`. Over values: neither side is a column or something the row draws already (title, line, figure, status, deadline), two numbers share a notation, no column is long text, never on a checklist layout. |\n| `block.current` | A child List\'s `current` names a date or number of the child (stored, or a formula yielding one) that orders a set of revisions: the row holding the latest value is current, the rest fold as superseded. Never with `group`, `under`, `expect`, `checklist`, `compare`, `post` or `pick`, nor beside a column of long text. |\n| `block.checklist` | A child List with `layout: { checklist }` reads a task entity\'s rows as checks \u2014 never under documents, grouped, edited in place or picked together by an Act over several, its columns no files; `photo` names a files field of the child, `clause` a one-row link of it, `failed` options of its status, none `closed`; `passed` a yes/no of it, stored or a formula, setting each mark (yes passed, no failed, empty still to check) and standing without `task` or a status. With `expect` the lines of a standard are the checks, each marked and filled by the block\'s columns (a measured value, a note, a photo), so `photo` and `clause` are not named. |\n| `block.pick` | A child List\'s `pick` links rows of the child through `via` \u2014 an optional one-row link a person writes, no Act writes, no other rule compares rows by and no `under` List stands lines under \u2014 paired with a many-link of the record its own save writes; the child\'s `read_scope` reads a row naming no record through `via`; never with `expect` or `under`, nor where the App\'s saves leave out the child or the many-link. |\n| `block.narrow` | A child List\'s `filters`, `search`, `group` and `sort` follow the register List\'s rules over the child\'s rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, nor on an `expect` List; an `under` List is grouped by its documents. |\n| `block.expect` | `expect` names the child\'s title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the List; its `columns` name a field filled in place \u2014 unless the List\'s layout is a desk, whose lines are ticked \u2014 and its `create` takes the file where the child is pictured by one. |\n| `block.expect-fills` | An expected line is made from its key and its first filled column \u2014 a desk\'s from its key alone, by its tick \u2014 so every other field the child requires has a default, a start or a `default_from`, or is read first. |\n| `block.expect-of` | An `expect` object\'s `of` or `only` needs a select: `of` names the record\'s multi-select of the title\'s options; `only` names options of a single select, never with `of` or a `where` on it, one List per option; an Act\'s `paper` is drawn in one. |\n| `block.post` | A child List with `layout: { desk }` needs `expect` on a one-row link \u2014 never a select, nor with `under`, `pick`, a checklist layout, `compare` or a `create` list, its line made by a tick from its key alone: its `bodies` are plain text fields of the record a person writes, drawn nowhere else on it; its `text` is a text of the child, stored or a formula yielding text \u2014 a formula reading only stored values of its row, lookups through the link back to the record or through `expect`, and formulas of the row over those, never the clock; its `media` a files field of the record and its `link` a text field of the record; its `network` a single select of the entity the link names, and `networks` holds options of that select, by alias, each to one of facebook, instagram, threads, x, linkedin; its `group` is another single select of that entity. |\n| `block.funnel` | A Reading\'s `steps` (a funnel) are 2 to 8 numbers of its child, each named once \u2014 stored, a formula or a rollup \u2014 all in one notation, never a percent nor a unit read on each row. |\n| `block.list` | A read-only child List (`same`, `app`, or a Record that is `edit: false`) lists the rows of an entity that link to the record by its one link to it (`via` where it has several); with `same`, a one-row link of the record, it lists the rows sharing that link\'s row with the record \u2014 the record left out \u2014 by the listed entity\'s one link to that row\'s entity (`via` where it has several). `same` stands in a Record, never on a dashboard. Its Record\'s Fields \u2014 at least one, each a field of the listed entity named once, never its title nor one of the `columns` \u2014 are what a row shows, read-only, when it is opened in place. |\n| `block.list-opens` | A read-only child List\'s row opens in the record\'s drawer as a record: its entity is one a List of the App draws \u2014 the register\'s, a Book\'s, or a log\'s \u2014 or the List\'s `app` names the register app that works it. |\n| `block.figures` | A Record\'s own figures (Readings with no `entity` and no `by`, whose `value` names the field) are 2 to 8 numbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number the App\'s saves leave out \u2014 each placed once on the record. |\n| `block.agenda` | An agenda List\'s child names a date on its row line (`records` title or subtitle); the layout\'s `start` is a date of the record. |\n| `block.timeline` | A timeline List\'s child holds a date to place its entries by. |\n| `block.create-boolean` | A document\'s and a roster\'s cells\' `create` is `true` or `false`: their add asks the table\'s defaults. |\n| `block.via-path` | A `<link>.<link>` `via` reaches rows through a row of their own and only reads them: a List writing its rows lists a child linked to the row it stands under. |\n| `block.read-label` | A List that only reads its rows \u2014 a read-only child List, an Outside\'s List, a dashboard\'s List \u2014 states its `label`. |\n| `block.create-empty` | A child List that adds rows asks the fields of its add: where the table\'s defaults ask none, state `create: false`, or the fields it asks. |\n| `block.read-scope` | A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history\'s, which takes its record\'s. |\n| `block.agenda-note` | *Noted, never refused:* Rows each on a day and within it (at an hour, in a part of the day) listed as a table: a List with `layout: { agenda }` draws them by day. |\n| `block.under-note` | *Noted, never refused:* Rows holding a paper that cover another List\'s lines, listed apart with no add of their own stated: `under` draws them over the lines they cover. |\n| `block.timeline-note` | *Noted, never refused:* Rows each what happened on a day and who \u2014 a date on their line, a member, no status and no figure \u2014 listed as a table: a List with `layout: "timeline"` draws them as a log, newest first. |\n| `block.expect-note` | *Noted, never refused:* Rows each a kind of paper, listed with no `expect` and no `where` on the kind, that no Act files a call into or `fills`: the lines still missing read as what a record needs. |\n\n<!-- generated:end rules-block -->\n\n## Record rules\n\n<!-- generated:start rules-record -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.placed-once` | A Field, a child List, a Reading, a Statement and an Act each stand once in a Record: its head draws the title, subtitle, image, status, figure and folder, and a field\'s one other place is a part\'s Fields, a gallery or prose Field (`block`) or a Statement \u2014 save the image, which one files gallery may draw again, whole, the figure, which a Statement draws as what it comes to, and rows one List prices and another compares (`compare`); two places whose `when`s never hold together (a select at options the other\'s lacks) each draw it, and a plain child List (no `expect`, `under`, `pick`, `compare`, nor a `layout` of desk or checklist) likewise. |\n| `record.foot-total` | A child List\'s foot totals the child\'s figure and each summed column over the rows it lists; the record\'s rollup summing that field over those rows (the same `where`) is that foot, never also a Field of a part. |\n| `record.fact-restates` | A Field of a part never looks up what its link already reads where it stands: the linked row\'s title beside the link, the figure or bound its meter reads on the head\'s line. |\n| `record.section-holds` | A part (Section, Side, Page) holds Fields, child Lists, Readings, Progress, Statements or Acts. |\n| `record.compose` | A Record\'s `compose` is stated instead of parts (it holds no children), on a page `door`, by an App with no Books whose records have no owned stages (`status.stages`) and no picture (`image`) \u2014 a composed page draws neither. Each name is a field of the entity, an Act of the app on the record or a child entity linked to it; one `Title`, and each field, Act and child drawn once \u2014 a field by `Field` or `Document`, never the title or the status, which `Title` and `Status` draw. The first `Act` the page itself draws is its next step and every other one secondary; an `Act` is a button alone, never one making a set of papers. A `Bar` holds no `Rows` and no `Document`, and an `Act` in it that destroys or corrects what is done (`danger`, or a move back) stands in the record\'s \u22EF instead (the Bar piece\'s own row says how it draws). A `Panel` draws nothing in the page: it is one entry of the record\'s \u22EF, named by its `title`, opening its pieces in a panel beside the page; it stands directly in the root `Column`, holds `Rows`, `Field`, `Act`, `Group` and `Document` \u2014 never `Title`, `Status`, `Bar`, `Copy` or another `Panel` \u2014 and an `Act` directly in it ends the panel\'s name, secondary. A `Group` is headed once, by its `title`: holding one `Rows`, that heading carries the rows\' count and their add in place of the rows\' own, and the `Act`s the group holds end its line. A `Copy` takes a text field\'s words as the page holds them now, a `Document`\'s words not yet saved among them; a field a `Copy` alone names is read, never written. |\n| `record.sections` | *Noted by the designer on a write, never refused:* A List laid out as a table, cards or a gantt opens a Record whose Sections hold the fields people write, a files Field per paper, a child List per child and the record\'s Acts. |\n| `record.header-budget` | *Noted by the designer on a write, never refused:* A Record\'s head says what the row is and where it stands \u2014 its title, line, picture and status \u2014 and at most two more facts, one of them a deadline at most; the rest stands in a Section. |\n| `record.children` | *Noted by the designer on a write, never refused:* A table some App records, whose rows other rows each belong to (a one-row link to it, never its status history), draws some of them under its row \u2014 a child List in its Record, a List with `layout` roster or lanes by that link, or an App over them working inside one of its rows at a time (`scope`). |\n| `record.at` | A Section\'s `at` names options of the record\'s status; a record with no status has no steps. |\n| `record.at-once` | A stage is the work of one Section: two Sections naming one option of the status in `at` hold some Field to values apart in their `when` \u2014 a `when` holding the status keeps its Section off the options it leaves out. |\n| `record.section-page` | A Page stands only on a page `door` \u2014 never a drawer\'s or one beside the List \u2014 is never a step (a Page has no `at`), has a `title` naming an address no other Page shares (a letter or a digit at least), and is never every part. |\n| `record.side` | At most one Side stands in a Record, on a page `door`, and it is never also a Page. |\n| `record.beside` | A Record with `door: "beside"` opens beside a table List \u2014 never a board, cards or a calendar, which draw no list to stand beside. |\n| `record.history` | No part of a Record reads the entity\'s status history (`status.history`): the rail reads each move. |\n| `record.section-acts` | An Act in a part works on the record itself; an Act in a child List stands on the child\'s row, and an Act in the register List with an `on` on the register\'s rows. |\n| `record.act-in-section` | Every Act of the record stands in the part whose work it is; only a correction no part owns \u2014 `danger`, and no other status move offered at the same moment \u2014 or an Act an agent runs for the person (a `workflow` or an `intake` moving no status), placed in the List and in no part, waits in the head\'s \u22EF. |\n| `record.move-home` | A status move offered at one step stands at that step\'s foot when it moves the work forward, to an exit, or back to a step it passes anyway; one offered at several steps, or moving back into a detour, stands in the Section whose `at` holds the status it enters. A tree\'s status move written in a Section with no `at`, or in none, is placed where this puts it and said. |\n| `record.act-staged` | An Act at a staged Section\'s foot is offered only during that Section\'s steps: its `when` holds the status to options the Section\'s `at` names. A tree\'s Act with no `when` at the foot of a Section staged at one step is offered at that step and said; a correction stays offered at every step, and an exit is refused. |\n| `record.act-hidden` | An Act is never offered only while its part\'s `when` hides the part. |\n| `record.outside-place` | An Outside stands directly in a Record whose table states a stored status select, and its `key` is unique in the App: a link to a record names it. |\n| `record.outside-at` | An Outside\'s `at` names options of the record\'s status that no stage owns, and no two Outsides of a record open during the same option. |\n| `record.outside-field` | An Outside\'s Fields are read as values, each a field of the record shown once, never `edit`, `when` or a label of its own. |\n| `record.outside-list` | An Outside\'s Lists read the child rows under the record as they are: their `columns`, `label` and `where` only, never an add, a Delete, a pick, a filter, a search, an `app`, a link that opens, or a bound read from the record. |\n| `record.outside-act` | An Outside\'s Acts are its alone and each ends the visit: `when` holds the status to `at`, `set` moves it to an option outside `at`, `asks` are text, numbers, dates or listed options, `requires` names fields the Outside shows or the act asks, a `sign` is by a text field with no witness, and none states `on`, `of`, `workflow`, `templates`, `paper`, `intake`, `record`, `review`, `open`, `danger`, `recommends` or `me`. |\n| `record.field-edit` | An Act that only writes one field of its record, not the status, from the one input it asks is that field\'s edit: the Field rests in the part as a control. |\n| `record.holds` | A Record holds parts (Section, Side, Page) or the Fields of its one part, never both; the Record of a Book, or of a child List no row opens in place (an agenda, a timeline, a read-only List), holds Fields alone. |\n| `record.field-heading` | A Field drawn as a control states no `label` or `when`: only a gallery or prose Field (`block`) has a heading and a condition. |\n| `record.section-owners` | *Noted, never refused:* A Section holding acts offered at stages that different owners work \u2014 a role apiece, or a member field apiece \u2014 is two jobs in one: each owner\'s part is a Section of its own. |\n\n<!-- generated:end rules-record -->\n\n## Act rules\n\n<!-- generated:start rules-act -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.paper-note` | *Noted, never refused:* A made paper kept in a field of a record that lists its papers by kind: it is one of those lines. |\n| `act.moves` | *Noted by the designer on a write, never refused:* Rows a stored status stages towards `closed` options are moved by an Act of some App over them, or by its authored `workflow` \u2014 each open option moves to the next, the last to the first closed one. |\n| `act.set-not-asked` | A field an Act `set`s (an option, a value, `{ "now": true }`, `{ "me": true }`, an input) or `sign`s is the Act\'s outcome: the App\'s `create` never asks it, and a required one takes a `default`. A `formula` only starts a value, which a person may change after. |\n| `act.asks-set` | A field an Act `asks`, `set`s, `sign`s or `writes` is stated by one of them: what the Act writes is its outcome, never also asked, and no field is written by two clauses of one Act. |\n| `act.writes` | `writes` stands beside `workflow` and names stored fields of the acted row, each once, that no `set` (its own or another Act\'s) states. |\n| `act.starts` | `starts` (an ask `{ field, starts }`) starts a field the Act asks \u2014 `today` a date or a moment (never a span), `me` a member \u2014 for the reader pressing it, who changes it at will. |\n| `act.set-formula` | `set: { <field>: { formula } }` reads fields of the acted row and computes what the field holds \u2014 a number, a date, text, a yes/no or a single select\'s option (`{field:option}`), typed as the entity\'s own formulas are \u2014 never the status; a bound a write rule holds the field to is checked on the computed value as the Act runs. |\n| `act.of` | An Act in a child List works on that child: the one row pressed \u2014 or, with `on: "ticked"`, the rows ticked together where the record lists them as tasks \u2014 never the rows in view. |\n| `act.files-written` | An Act\'s `writes` names no files field: a file an Act makes is a paper, filed in a slot it names with `paper` (a child List with `expect`). |\n| `act.writes-line-key` | An Act never writes the link or the key naming an expected line under its record \u2014 a line\'s key is stated by filling it. |\n| `act.makes-or-runs` | An Act makes a document (`template`, `templates`) or runs an authored `workflow`, never both \u2014 the workflow makes what it needs. |\n| `act.does-nothing` | An Act does something: it `set`s a value, `asks` a field, runs a `workflow`, makes a document, or takes an `intake`, a `record` or a `sign`, or `open`s an app. |\n| `act.view` | An Act over every row in view (`on: "view"`) writes (`set`), makes a document or runs a workflow, and states no `requires`. |\n| `act.requires-drawn` | Every field an Act `requires` is drawn where the Act stands \u2014 its part\'s Fields, a child List\'s, its `asks`, or an unstaged part\'s where its own is unstaged \u2014 never the head alone. |\n| `act.requires-written` | A field an Act requires that only an Act writes is written by an Act offered together with it, or asked by the Act itself. |\n| `act.recommends` | A field an Act `recommends` is not also in its `requires`. |\n| `act.set` | `set` writes each field a value it holds \u2014 an option alias, a number, a yes/no, a text, `{ "now": true }` on a date, `{ "me": true }` on a member, `{ "input": "<name>" }` of an input the Act asks of the same kind \u2014 or clears an optional one with `null`. |\n| `act.into` | `into` is a files field of the one record the Act is pressed on, beside its `template` or `templates`. |\n| `act.paper-list` | A List a made paper names is one it can list: none keeps its rows by the reader, and no name is reached by a child List that writes its rows and one that only reads them, or by two of either, at once. |\n| `act.paper` | `paper` is a kind the record lists its papers by, on one record \u2014 never beside `into` or `templates`; one maker per kind. |\n| `act.templates` | `templates` names each template once, kept in `into`, on one record pressed alone \u2014 never beside `template` or `asks`, in a child List, or with an `on` over several rows. |\n| `act.intake` | `intake` names a child the record lists in a child List with `expect` on its title \u2014 a single select, the paper\'s kind \u2014 and is narrowed by no `where`, holding exactly one files field and needing nothing an added line does not fill; the Act runs on one record, states `fills`, and states nothing but its `label`, `when` and `requires` beside them. |\n| `act.record` | `record` names a child the record draws in a timeline List or a child List with no `expect`, `under` or `where`, linking back by one single link, its row needing nothing the recording does not write: `audio` and `video` files fields, `transcript` text, `at` a date, `by` a member, each its own field; on one record, stating only `label`, `when`, `requires` and `fills` beside it. |\n| `act.sign` | `sign` runs on one record, never a child\'s row: `into` a files field, `by` a text field the signer types or a member field set to the person pressing, `at` a date, `witness` a member field \u2014 stated exactly beside a text `by` \u2014 each a field of its own that the Act does not also ask or set, and no other Act signs into `into`. |\n| `act.open` | `open` opens another app at the record and does nothing else: an Act on one record of the app\'s own, stating only `label` and `when` beside it. |\n| `act.open-app` | An Act\'s `open` is the id of an app of this workspace that reaches the record\'s table: a register over it, or a custom-code app declaring no table or declaring it. |\n| `act.fills` | `fills` names each once, only beside `intake` or `record`. Beside `record`: a field of the recording\'s row the recording does not write, never a link. Beside `intake`: a field of the record, `<link>.<field>` on the row a required one-row link names (never beside that link), or a child the record adds rows of in a child List with no `expect`, `under` or `where`, filled in the fields its `create` asks \u2014 every one a new row needs among them. Each field filled is one a person writes \u2014 text, a number, a date, a yes/no, options or a single link\'s row \u2014 never a status, a field an Act writes or a link `same` narrows; a link\'s row is one the App reads of its entity. |\n| `act.fills-drawn` | *Noted, never refused:* A field `fills` writes that the record draws nowhere: the review shows the change, the record does not. |\n| `act.fills-new` | *Noted, never refused:* A child `fills` adds rows of, whose natural key the papers cannot state: every row read is added, so the same papers read twice list them twice. |\n| `act.input` | An input has a name of its own \u2014 never another ask\'s, a field\'s, or the record input its workflow takes (nor `approved` beside `review`) \u2014 `options` exactly when it is a select, `entity` exactly when it is a link, and a `default` field holding what it states. A `file` input is read by the Act\'s authored `workflow` alone, starts empty, and is never written into a field by `set`. |\n| `act.review` | `review: true` stands beside an authored `workflow` on an Act over the record, the ticked rows or every row in view, in an App over one entity \u2014 never in an App with Books, nor beside `set`, `template`, `templates`, `intake`, `record`, `sign` or `confirm`, nor in a child List, and it asks inputs only, none named `approved`. Its workflow is that Act\'s alone, run by no other Act. The workflow calls only steps that read \u2014 no agent \u2014 and returns a changeset of the entity\'s table in `data.review`, the same for the same inputs and rows; the platform writes the rows `approved` names, each only while the run proposes it as the caller read it. Over every row in view (`on: "view"`) it is handed no rows and states no `when`: its workflow reads the rows it may change, whatever the view is narrowed to, and no bound on a run\'s rows applies. |\n| `act.set-tagged` | An Act\'s `set` states the moment as `{ "now": true }`, the person pressing as `{ "me": true }` and an input as `{ "input": \u2026 }` \u2014 never the strings `"now"`, `"me"` or `"input:\u2026"`. |\n\n<!-- generated:end rules-act -->\n\n## What the workspace remembers\n\nThe workspace holds every table, field and select option under an id that never changes, whatever its\nlabel or alias becomes. `set_tables` answers each table\'s id, and an\napp\'s body names tables and templates by those ids, so a relabel on either side is a rename and no body\npoints at another table by accident.\n\nA table `set_tables` refuses to write, or an id the workspace no longer holds, is a refusal by name. `restore_table` puts a deleted table back.\n\n## A complete workspace\n\nTwo jobs, so two apps: a sales desk, where an order is taken and shipped, and an account review, where an account\nmanager reads each customer by tier. The split is per job, never per table: a table a job reads only in passing, such as\na price book, is a picker on that job\'s lines or an Act of that job\'s app (`first_build`, \xA7 3).\n\nOne `set_tables` call makes both tables: each item carries a `ref`, and a link names the table it points\nat by the `ref` of the other item. Its answer carries each table\'s id. One `set_app` call then makes each\napp, `{ "body": \u2026 }`: the tree\'s root names the app by its `key` and `name`, and the tables it uses in `tables` by those ids,\nshown here as placeholders to replace.\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": {\n "name": {"label": "Name", "required": true, "type": "text"},\n "logo": {"label": "Logo", "type": "files"},\n "tier": {\n "label": "Tier",\n "default": ["standard"],\n "type": "select",\n "options": [\n {"alias": "standard", "label": "Standard", "color": "slate"},\n {"alias": "gold", "label": "Gold", "color": "amber"}\n ]\n },\n "orders": {"label": "Orders", "type": "link", "pair": "customer", "table": "order", "multi": true},\n "total_ordered": {"label": "Total ordered", "type": "rollup", "via": "orders", "op": "sum", "field": "amount"}\n },\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "field_key": "tier",\n "type": "select",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{"field_key": "name", "order": "asc"}]\n }\n ],\n "title": "name",\n "records": {"image": "logo", "party": "organization", "figure": "total_ordered"}\n },\n {\n "ref": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": {\n "code": {"label": "Order no.", "type": "text", "unique": true},\n "placed_on": {"label": "Placed on", "type": "date", "format": "date"},\n "amount": {"label": "Amount", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "total": {\n "label": "Total with VAT",\n "type": "formula",\n "expression": "{amount} * 1.1",\n "notation": {"style": "currency", "currency": "VND"}\n },\n "customer": {"label": "Customer", "type": "link", "pair": "orders", "table": "customer", "multi": false},\n "note": {"label": "Note", "type": "text"},\n "stage": {\n "label": "Stage",\n "default": ["draft"],\n "type": "select",\n "options": [\n {"alias": "draft", "label": "Draft", "color": "slate"},\n {"alias": "confirmed", "label": "Confirmed", "color": "blue"},\n {"alias": "shipped", "label": "Shipped", "color": "green"}\n ]\n }\n },\n "title": "customer",\n "records": {\n "subtitle": ["code", "placed_on"],\n "figure": "amount",\n "status": {"field": "stage", "closed": ["shipped"]},\n "starts": {"placed_on": "today"}\n }\n }\n ]\n}\n```\n\n```json\n{\n "body": {\n "type": "App",\n "key": "account_review",\n "name": "Account review",\n "color": "blue",\n "tables": {"customer": "<id of the customer table>", "order": "<id of the order table>"},\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "customer",\n "columns": ["tier"],\n "children": [\n {"type": "Reading", "key": "ordered_by_tier", "by": "tier", "value": "total_ordered"},\n {\n "type": "Record",\n "children": [\n {"type": "Section", "key": "account", "title": "Account", "children": ["tier"]},\n {\n "type": "Section",\n "key": "orders",\n "title": "Orders",\n "children": [{"type": "List", "entity": "order", "columns": ["total"], "create": ["code", "amount"]}]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n```json\n{\n "body": {\n "type": "App",\n "key": "sales_desk",\n "name": "Sales desk",\n "color": "violet",\n "tables": {"customer": "<id of the customer table>", "order": "<id of the order table>"},\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "order",\n "filters": ["customer"],\n "create": ["code", "customer", "placed_on"],\n "children": [\n {"type": "Reading", "key": "amount_per_period", "by": "placed_on", "value": "amount"},\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {\n "type": "Section",\n "key": "order",\n "title": "Order",\n "children": [\n "note",\n {\n "type": "Act",\n "key": "confirm",\n "label": "Confirm",\n "when": {"stage": ["draft"]},\n "set": {"stage": "confirmed"}\n },\n {\n "type": "Act",\n "key": "ship",\n "label": "Ship",\n "when": {"stage": ["confirmed"]},\n "set": {"stage": "shipped"}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\nThe account review leaves the door a record opens through and the rows it opens in to the system, each the default the\ntable\'s facts give; the sales desk states a drawer. Each List names what its add asks (`create`). The customer\'s orders are a block in its "Orders" section, never its own `Orders`\nlink as a field as well: one fact, one place. An order is titled by its customer and read by its number under it; the\nList names what its add asks, since a subtitle or a figure is never asked by default. `Total ordered` is a rollup, so it\nis read and never asked; the sales desk\'s record opens in a drawer, because its rows are worked one after another. An\norder\'s `stage` is its status (`records.status`), moved only by the app\'s acts: they set it, so the app\'s saves leave it\nout, each act names the stage it is offered at (`when`) and the one it sets, and the register opens on the orders not yet\nshipped (`closed`).\n';
1764
+ // src/reference.md
1765
+ var reference_default = '# Tables and apps\n\nThe tables of a workspace and the apps over them, each a JSON document. A table is a document, an item of\nthe tool `set_tables`: its fields, options, views, how a row is recognised, and the rules its writes\nfollow. An app is a tree of components, the `body` of the tool `set_app`: one working register over one table, or a\ndashboard of readings. Each \xA7 is its own page, `reference/<section>`.\nWhich treatment each job\'s screen needs: the `design` reference. Each treatment worked through in an\napp: the `examples` reference.\n\n**What the documents compose with**\n\n- **Tables and fields** (\xA7 Tables, \xA7 Read scope, \xA7 Field) \u2014 a table\'s columns, options and links. A table document sits\n in the `tables` list of a `set_tables` call; a field sits under its alias in the table\'s `fields`.\n- **Records** (\xA7 Records, then \xA7 Status) \u2014 how a row of each table is RECOGNISED: its title, the line\n under it, its picture, its status, the one number it stands for. Stated once per table\n and read by every surface that draws one of its rows.\n- **Write rules** (\xA7 Write rules) \u2014 what a write finds, bounds and picks among.\n- **Apps** (\xA7 App tree, then a page each: \xA7 Changing an app, \xA7 What an app may write, \xA7 App node, \xA7 List node, \xA7 Layouts, \xA7 Time and place layouts, \xA7 Publish desk,\n \xA7 Record node, \xA7 Section node, \xA7 Side node, \xA7 Page node, \xA7 Outside node, \xA7 Tab node, \xA7 Field node, \xA7 Act node, \xA7 Check node,\n \xA7 Reading node, \xA7 Progress node, \xA7 Statement node, \xA7 Export node, \xA7 Book node, \xA7 Query node, \xA7 Workflow node,\n \xA7 Composed page, \xA7 Tasks, \xA7 App rules, \xA7 Reading rules, \xA7 Statement rules, \xA7 Check rules, \xA7 List rules, \xA7 Record rules, \xA7 Act rules, \xA7 Template rules) \u2014 one working register over one table and the record each row opens,\n with the logs beside it: which fields go where, the acts, and the checks that guard them \u2014 or a dashboard of readings.\n How each thing LOOKS is the runtime\'s, one treatment per concept; no prop here changes it.\n A screen the job needs and no component states is told to the person \u2014 the job, what the app\n drew, the word wanted \u2014 never props bent to approximate it.\n\n**The working order**\n\n1. The tables and fields the jobs touch, and a `records` entry for every table an\n app lists, opens or picks. Which apps a business needs is `first_build`, \xA7 3.\n2. One `set_tables` call with the tables: a link names the table it points at by the `ref` of another\n item of the call, and the answer carries every table id and the aliases of the fields it added. A\n call carries at most 50 tables, 200 first rows a table, and 2000 rows and 2000 documents in all; more\n tables go in a further call, which names the earlier tables by alias. A call is checked whole and writes\n nothing when refused, listing everything wrong; `dry_run` is for reading the grants a write would make or\n deciding `force`.\n3. One `set_app` call per app. The tree names its tables and document templates by alias wherever it points at one;\n the App node\'s `tables` and `templates` maps are filled by `set_app`, never written.\n `set_app` with the tree as `body` creates the app, named by its `name`; a refused tree writes nothing and is listed whole, and `dry_run` is for reading the permissions the write\n would grant and remove (on the CLI, `lotics app apply <file>`, with `--check` for `dry_run`).\n4. Change an app afterwards one node at a time: `get_app` prints the `tree:`, each node\'s address under `node addresses:`\n and the `version` line, which is the `base_version_id` to send (the tree and addresses go to the app\'s author alone);\n `set_app` with `app_id`, `base_version_id` and `nodes`, a map from a node\'s address to a\n patch, changes it (`reference/changing-an-app`), and its answer states the permissions the change grants and removes. A table\n is changed by `set_tables` with its `table_id`, a merge patch over the document `get_table` answers. To go back,\n send the tree you saved before the change as `body` with `app_id`: it replaces `children` but keeps every App prop it\n omits (`words`, `scope`, `period`, `icon`), so null those. (On the CLI, `lotics app pull <app_id>` writes the tree as JSON and `lotics app apply\n <file>` sends an edited file, refused when the app has moved past the version the file was pulled at.) An app\n saved before apps were trees has no tree: `get_app` says so beside what it lets a person write, and `set_app` with\n `app_id` changes it by its whole tree as `body`, which replaces it, or by its name, description, icon and color alone.\n\n## The rules\n\n- **A `set_tables` call is all or nothing.** Every item is checked before any is written; an item\n without `table_id` makes a table, one with it patches that table.\n- **An app\'s labels decide its language.** A body names no locale \u2014 the words an\n app draws itself (its buttons, empty states, refusals) are in the language its\n own labels are written in, so a Vietnamese desk beside English ones answers in\n Vietnamese. The generated screens read the kit\'s\n pack, and a generated WRITE cannot: its refusals run on the server, so they are\n worded in that same language. Mix the two within one app and it answers in two. The app\'s own `words`\n (its words for a table, a field or an option) count as its labels: Vietnamese words over an English table\n make the app answer in Vietnamese.\n- **`set_tables` and `set_app` check all of it before anything is written**, and\n report every problem in one run rather than the first. The tables half of this\n page ends its keys with the rules the check enforces there, and \xA7 App rules and the pages after it hold the app\'s, one sentence each\n under an id; a finding names its rule\'s id (`[list.filter]`), and\n `reference/<rule id>` is that one rule\'s page.\n\n## Top level\n\nEvery table of keys on this page is generated from the grammar a table document and an app tree are written in, so it is the whole of what a key may hold.\n\n<!-- generated:start top-level -->\n\n#### set_tables call\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tables` | list of [Table item](#table-item) | yes | One or more, all or nothing; each item is a table document with these keys added. It creates a table (a `label` and `fields` at least) or, with `table_id`, patches one: the document it states merges into the table\'s own |\n| `rows` | map of alias \u2192 list of [Row](#row) | no | First records of the tables the call creates, keyed by each one\'s `ref` |\n| `force` | boolean | no | Consent to losing what rows hold: a field deleted, an option removed |\n| `dry_run` | boolean | no | Answer what the call would change, writing nothing: for reading the grants a write would make or deciding `force`. A refused call writes nothing too and lists every reason, so a dry run checks no more than the call does |\n\n#### Table item\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | alias | no | A created table\'s handle in this call: other items link to it, and `rows` are keyed by it |\n| `table_id` | text | no | The table to patch; absent, the item creates one. A patch states only what changes: `null` removes a key, a list replaces whole |\n\n<!-- generated:end top-level -->\n\n**A `set_tables` item is a table document** (\xA7 Tables) with `ref` and `table_id` added, and the call\ntakes no other key. **A `set_app` body is an app tree** (\xA7 App tree), whose root is an App node: its `tables` and\n`templates` each map a local alias the tree names to the id of the table or document template it means, from the\n`set_tables` answer or `get_table`. The tree is the only home of the app\'s `name`, `description`, `icon` and `color`;\n`set_app` takes it whole to create, or as a merge patch over the App node with `app_id`, or `nodes` with `app_id` to change one, and with `base_version_id` (the\nversion you read) it refuses a write over a newer one. Nothing is built code. An unknown key is an error, never ignored.\n\nA smallest call, two tables of plain fields, a select and a two-way link \u2014 nothing else is needed to write it:\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": {\n "name": {"type": "text", "label": "Name", "required": true},\n "tier": {"type": "select", "label": "Tier", "options": [{"alias": "gold", "label": "Gold", "color": "yellow"}, {"alias": "standard", "label": "Standard", "color": "gray"}]},\n "orders": {"type": "link", "label": "Orders", "table": "order", "multi": true, "pair": "customer"}\n }\n },\n {\n "ref": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": {\n "code": {"type": "text", "label": "Code", "required": true},\n "customer": {"type": "link", "label": "Customer", "table": "customer", "multi": false, "pair": "orders"},\n "amount": {"type": "number", "label": "Amount"}\n }\n }\n ]\n}\n```\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how a document and an app tree\nname a field, an option, a view or a table; they are never shown to anyone. `label` is\nwhat a person sees.\n\nLabels must be unique within their namespace \u2014 two fields on one table, two options\non one field, two views on one table cannot share a label.\n\n## Tables\n\n<!-- generated:start table -->\n\n#### Table document\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | yes | The table\'s name in the workspace |\n| `singular` | text | no | One row of this table, in the business\'s own words \u2014 what a create\'s button and panel name |\n| `description` | text | no | The table\'s description, written onto the table in the workspace, where a caller of an app\'s queries and workflows reads it beside them |\n| `fields` | map of alias \u2192 [Field](#field) | yes | The table\'s columns, each under its alias, which is its key here and so is never stated inside it |\n| `read_scope` | [Read scope rule](#read-scope-rule) | no | Which rows a member reads. Absent, every member with access to the table reads every row. |\n| `unique` | list (at least one) of list of alias (at least one) | no | Sets of fields whose values no two live rows share \u2014 each a list of field aliases (text, number, date, a single select, or a link holding one row \u2014 `multi: false`); a single text field states `unique: true` on itself instead of a one-field set. A create or update landing a second row with the same values is refused. |\n| `views` | list of [View](#view) | no | Saved views, in the order they are listed; with none, the table still opens on its default grid |\n| `title` | alias | no | The alias of the field that names a row wherever a link or a record shows one; absent, the first text field |\n| `colors` | any value | no | Row colors: a list of { filters, color }, the filter reading fields of this table by alias and a select\'s options by alias |\n| `records` | any value | no | How a row is recognised: the keys of the Record, Status and Cadence tables below but `title`, which sits above; a table they name is an id or a `ref`. A status `history` that names a `ref` no item of the call declares is made by the call as a table, `dates` add each date field they name, and a status with stages each stage and each open option they name none for its `<option>_at`, unless the document declares it; each is listed in the result\'s notes |\n| `natural_key` | list of alias (at least one) | no | The field aliases a row of this table is RECOGNISED by: an app\'s add and find-or-create reuse the row they find by these values, minting one only where nothing matches. A record tool or the API creates the row it is handed. |\n| `no_overlap` | [No overlap](#no-overlap) | no | No two rows hold overlapping spans \u2014 a create or update that would is refused, and so is a change to the dates or states of the row a span is read through that would make two rows overlap |\n| `checks` | map of alias \u2192 table check (`workflows/table-checks`) | no | The conditions a write of the table\'s rows may not meet, each under its alias: a merge patch edits one, and `null` removes it. Expressions name fields by alias |\n| `workflows` | map of alias \u2192 table workflow (`workflows/table-workflows`) | no | The effects that follow every committed write of the table\'s rows \u2014 the grid\'s, an import\'s, an agent\'s, any app\'s \u2014 each under its alias: a merge patch edits one, and `null` removes it. A body names fields, options and tables by alias |\n\n<!-- generated:end table -->\n\n<!-- generated:start rules-entity -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `entity.singular` | A table a List\'s `create` is mounted over \u2014 the register or a child List that adds rows \u2014 states its `singular`: one row of it, in the business\'s words. |\n| `entity.required-cycle` | Required links never wait on each other in a loop: no row of the loop could be created first. |\n| `entity.link-format` | A text field of `format: "link"` holds web addresses; one whose rows hold a phone or a mail address states no format. |\n| `entity.read-scope` | A `read_scope` clause names a group (`member_of`) or a field of its table reached through one-row links: `"self"` a member field, options a single select declares; an `any` or an `all` groups clauses, and groups nest. |\n| `entity.unique` | A `unique` set names two fields or more of its table once each, each holding one value to compare \u2014 text, a number, a date, a single select, a one-row link \u2014 and is stated once; one field alone is `unique: true` on it. |\n\n<!-- generated:end rules-entity -->\n\n**`unique` is a set of values no two live rows share.** Each entry names fields\nof this table holding ONE value \u2014 text, number, date, a single select, a link\nwith `multi: false` \u2014 and a create or update landing a second row with the\nsame values is refused. A set of one text field is that field\'s own `unique:\ntrue`, so it is refused here. A create carries each set in the names its panel\nsends, so the panel can name the duplicate before the write does.\n\n**`singular` is what a create says** \u2014 `New Order`, `Add Claim line`, `H\u1ED3 s\u01A1\nm\u1EDBi` \u2014 while `label` names the table, so without it the button reads `New\nOrders`. Nothing derives it: English plurals are irregular, and no language is\nexempt. In a language without plural forms it is usually the label itself,\nless any word for the collection. A List that leaves `create` to its default adds nothing over a table that states\nnone (nor does the app\'s `words` state one); a `create` list stated over such a table is refused (`entity.singular`).\n\n**An app that only reads a table\'s rows draws them with `edit: false`** and states `create: false` and `remove: false` \u2014 a log of what the system sent, a copy of\nwhat another system holds. The app keeps the section and opens each row at rest, with no Add and no Delete; a party\nof it is picked, never found or minted; and the app writes no create, update or remove for it. It shapes that app, not who may write: a direct write \u2014 the records\nAPI, the chat agent, an import \u2014 follows the table\'s access, as on any table, and another app over the same rows\nstates its own tree.\n\n**`read_scope`** \u2014 which rows a member reads \u2014 is its own page, `reference/read-scope`.\n\n## Read scope\n\n<!-- generated:start read-scope -->\n\n#### Read scope rule\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `any` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option) \\| [Read scope rule](#read-scope-rule)) (at least one) | no | A row is readable when ANY of these holds |\n| `all` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option) \\| [Read scope rule](#read-scope-rule)) (at least one) | no | A row is readable when ALL of these hold |\n\n#### Read scope by role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `member_of` | text | yes | The id of a group (from Settings, or chat): whoever is in it reads the row |\n\n#### Read scope by member\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | link aliases from this entity outward, each holding one row (`multi: false`) \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A member field alias on this entity, or on the entity `through` lands on |\n| `is` | `"self"` | yes | The members this column names on a row read that row |\n\n#### Read scope by option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | link aliases from this entity outward, each holding one row (`multi: false`) \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A single-select field alias on this entity, or on the entity `through` lands on |\n| `is` | list of alias (at least one) | yes | Its option aliases whose rows are readable \u2014 naming none would hide every row |\n\n<!-- generated:end read-scope -->\n\n**`read_scope` is a ROW rule, enforced by the platform.** It is resolved into the table\'s own row\nfilters, so an app, a workflow reading for a viewer, and the API all answer the same rows \u2014 a per-record\nvisibility field the app merely honours is a convention, not a gate. A `"self"` clause reads a column\nof one member or several, and every group and option a clause names must exist.\n\n**An app reads every table under the table\'s own `read_scope`**, and a tree has no key to lift it. A desk whose audience\nits sharing already decides is let into the rule by a group that holds that audience (`member_of`), stated on the\ntable.\n\n**A row rule is never inherited.** A child read under a private record states\nits own: the record\'s `member_of` clauses as they are, and its column clauses\n`through` the one-row link the child hangs by \u2014 over one hop or several, each a\nlink with `"multi": false`, since over several rows the rule would admit a\nreader any one of them admits while the editor\'s guard reads the first. Only the\nstatus history takes its record\'s rule.\n\n## Field\n\nEvery field carries the keys below, and its `type`\'s section adds the rest; a\ntype whose section names no `default` takes none. `label` may not contain `{` or\n`}` (formulas reference fields by label at the platform level). Every row in `rows` states each\n`required` field it carries (neither a default nor a `default_from` copy is applied to them), and a required\nLINK is written with its row: the table it names is created first, and tables\nwhose required links name each other are refused, since none of their rows could\never be created.\n\n**A table states who keys nothing.** Whether a person may change a field in an app is that app\'s statement\n(what its tree draws, and `edit: false`, \xA7 App tree): a post\'s id a publishing workflow brings back, a count a sync fills, a status an act moves\nare left out of the apps that do not save them, and written by a workflow, the records API, the chat agent and an\nimport as the table\'s access allows. A formula, a rollup, a lookup and an autonumber are no one\'s to key already.\n\n<!-- generated:start field -->\n\n#### Field\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"date"` \\| `"boolean"` \\| `"select"` \\| `"member"` \\| `"link"` \\| `"files"` \\| `"formula"` \\| `"rollup"` \\| `"lookup"` \\| `"autonumber"` | yes | What the field holds \u2014 each type takes the further keys its own section lists |\n| `label` | text | yes | The field\'s name in the workspace |\n| `description` | text | no | The field\'s description, written onto the field in the workspace |\n| `required` | boolean | no | Refuse a record whose cell for this field is empty. Every write path \u2014 create, update, an agent\'s tool call, a workflow\'s set \u2014 refuses the row by field name. A stored field is flagged only once every row holds a value. |\n\n<!-- generated:end field -->\n\n### `text`\n\n<!-- generated:start field-text -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `unique` | boolean | no | Unique values required |\n| `format` | `"text"` \\| `"link"` \\| `"markdown"` | no | How the words are drawn \u2014 plain, as a link that opens, or as markdown |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `suggest` | [Suggest](#suggest) | no | A text field whose control offers, as the reader types, the distinct values a text field of a catalog table holds (a damage position\'s code among the codes on file); any text is still written |\n\n<!-- generated:end field-text -->\n\n### `number`\n\n<!-- generated:start field-number -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | number | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. Omitted, a decimal. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (its notation\'s `per_row`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n\n<!-- generated:end field-number -->\n\n`notation` is how the number reads, and every surface reads it:\n`{ "style": "currency", "currency": "USD" }` prints as money in its code,\n`{ "style": "unit", "unit": "kg" }` as a quantity with its unit after it,\n`{ "style": "unit", "unit": "percent" }` as a percent \u2014 10 is "10%", never\nmultiplied \u2014 and `{ "style": "decimal" }`, or no notation, as a plain number. One\nkey, so a figure reads one way.\n\nAn absent number is drawn blank, except a `sum` or count rollup read as a meter\'s measure: nothing accumulated yet reads 0.\n\nA unit \u2014 `{ "style": "unit", "unit": "kg" }` \u2014 is what a figure counts or\nmeasures; `percent` aside, it is measured or counted. A **measured** unit is a code of one catalog \u2014 mass `g`\n`kg` `t`, volume `l` `m3` `cbm` (a cubic metre under freight\'s name), length\n`mm` `cm` `m` `km`, area `m2`, duration `min` `h` `day` \u2014 so a figure typed in\nanother unit of its dimension converts (`12,5 t` into a `kg` field is 12 500),\nand a tile or a chart reads it in the largest unit it reaches (12 500 kg as\n12,5 t\u1EA5n; CBM never scales) while a cell, a fact and a column always read in the\nfield\'s own. Any other noun of at most 12 characters (`ki\u1EC7n`, `pallet`, `TEU`) is a\n**counted** unit: a word after the figure that never converts. A measured unit is\nwritten as its code \u2014 `t\u1EA5n`, `KG` or `m\xB3` is refused, naming the code. A formula\nstates its own as its `notation`; a rollup that keeps the value (`sum`, `avg`,\n`median`, `min`, `max`, `range`) and a lookup carry the unit of the figure they\nread, exactly as they carry a currency, and a count carries none. A quantity is a\nnumber with its unit, never words (`3 cartons` in a text field): only a number\nsums, converts and reads down a column.\n\nA field\'s cells always hold figures in its own unit, so moving it between two\nunits of one dimension (`kg` to `t`) converts every stored figure \u2014 a change made\nin the workspace, in the field\'s settings (which say how many first) or through\n`set_tables`. Any other change of unit\nrelabels.\n\nA figure whose unit or currency varies by row names a single select of its own\nrow as `per_row` \u2014 `{ "style": "unit", "unit": { "per_row": "uom" } }`,\n`{ "style": "currency", "currency": { "per_row": "currency" } }` \u2014 or a lookup of\none through a one-link, a line reading its shipment\'s currency. That select\'s\noptions are the vocabulary: each label a unit (`chi\u1EBFc`, `kg`), or an ISO 4217 code\n(`USD`). A label outside it is refused when the figure is written and when the\nselect is \u2014 its options, its type or its deletion while a figure names it. A row\nwhose select is empty reads its figure bare. Every app reads each row\'s figure in\nits own row\'s unit, and a sum never mixes units: totals are one per unit (past three units a total the runtime draws over a set of rows \u2014\nsummary line, group totals and feet, a dashboard\'s table foot \u2014\nstates the count alone; a reading an author states draws every unit), a chart draws one at a time. A rollup that keeps the value (`sum`, `avg`, `min`\u2026)\nover such a figure stands only where the child\'s select is a lookup, through\nthe rollup\'s own link, of a select on this table \u2014 the rollup reads that\nselect\'s unit; otherwise it is refused. A lookup of such a figure stands only\nwhere its table also looks up the figure\'s select through the same one-link,\nand reads in that lookup; otherwise it is refused. A count is unaffected. Moving a field between a\nfixed and a per-row unit relabels.\n\n### `date`\n\n<!-- generated:start field-date -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. A date string in the field\'s format. |\n| `format` | `"date"` \\| `"datetime"` \\| `"date_range"` \\| `"datetime_range"` | no | Whether the field holds a day or a moment, alone or as a span |\n| `timezone` | text | no | IANA timezone |\n| `derive_from` | `"created_at"` \\| `"updated_at"` | no | Auto-populate from the row\'s system timestamp; the field becomes read-only. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (its notation\'s `per_row`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n\n<!-- generated:end field-date -->\n\nA `default` is refused beside `derive_from`: the platform stamps that date.\n\n### `boolean`\n\n<!-- generated:start field-boolean -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | boolean | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-boolean -->\n\n### `select`\n\n<!-- generated:start field-select -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | list of alias | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. Option alias(es) this field declares \u2014 one for single-select. |\n| `options` | list of [Select option](#select-option) (at least one) | yes | The choices, in the order every picker and every ladder lists them |\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n#### Select option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable local alias, unique within the field |\n| `label` | text | yes | Display label for the option |\n| `color` | [colour](#select) | no | The colour the option\'s badge is drawn in; absent, the platform gives it one no sibling option holds |\n| `mark` | [mark](#select) | no | The option\'s own mark, drawn in place of its colour dot wherever the option is shown: the brand it is ({kind: "brand", name: one of facebook, instagram, threads, meta, tiktok, google-ads, zalo, linkedin, x, google-meet, youtube, telegram, whatsapp, gmail, google-drive, outlook, kiotviet, misa, lark, payos}) a kit glyph ({kind: "icon", name: "wrench"}) or the face of who it is, drawn from its label ({kind: "avatar", name: "person" or "organization"}). Every option of a field has one, or none does. A mark a reader does not draw falls back to the dot |\n\n<!-- generated:end field-select -->\n\nA patch of a select states its whole `options` list: a list replaces whole, so an option it leaves out is deleted. To\nrename or recolour one option, restate every option kept, each with its alias. Rows still holding a deleted option refuse\nthe patch unless `force`.\n\n```json\n{\n "options": [\n { "alias": "draft", "label": "Draft" },\n { "alias": "confirmed", "label": "Confirmed" },\n { "alias": "shipped", "label": "Shipped", "color": "green" }\n ]\n}\n```\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\nEvery option is drawn in its colour wherever it shows \u2014 a cell, a row\'s line,\na record, a picker, the bar a reading splits the rows by.\n\nAn option may carry its own `mark`, drawn in place of its colour dot wherever\nthe option is shown \u2014 a stage, a chip, a filter, a fact, an entry of a log, a\nreading\'s part, a lookup of the select on another table. Where the select is its\nrows\' `records` `mark`, a brand or glyph stands on the row\'s lead tile in the option\'s\ncolour, and an option with none leads with its colour dot there. A select a reader scans\ndown a column \u2014 how a payment was made, the channel, the mode \u2014 states one on every\noption, since a glyph reads before its word: the brand it IS\n(`{"kind": "brand", "name": "tiktok"}` \u2014 one of `facebook`, `instagram`,\n`threads`, `meta`, `tiktok`, `google-ads`, `zalo`, `linkedin`, `x`,\n`google-meet`, `youtube`, `telegram`, `whatsapp`, `gmail`, `google-drive`,\n`outlook`, `kiotviet`, `misa`, `lark`, `payos`), a glyph the kit draws\n(`{"kind": "icon", "name": "wrench"}`; any other name is refused), or \u2014 where the\noptions name people or organisations who are no member and no row \u2014 the face of who\nit is, drawn from its label (`{"kind": "avatar", "name": "person"}` or\n`"organization"`). Every option of a select has one, or none does: a run of chips\nwhere one carries no mark reads as the one missing something. `set_tables` writes the\nmarks onto the table. The glyph names are `reference/field/option-glyphs`.\n\n```jsonc\n"options": [\n { "alias": "short_video", "label": "Short video", "color": "zinc", "mark": { "kind": "brand", "name": "tiktok" } },\n { "alias": "print", "label": "Print", "color": "amber", "mark": { "kind": "icon", "name": "newspaper" } }\n]\n```\n\n### Option glyphs\n\nThe kit\'s glyph names an option\'s `mark` takes:\n\n<!-- generated:start field-select-glyphs -->\n\nactivity, align-center, align-left, align-right, align-vertical-justify-center, align-vertical-justify-end, align-vertical-justify-start, arrow-down, arrow-down-a-z, arrow-down-up, arrow-down-wide-narrow, arrow-left, arrow-left-from-line, arrow-right, arrow-right-from-line, arrow-right-left, arrow-up, arrow-up-down, arrow-up-wide-narrow, arrow-up-z-a, ban, banknote, bed, bell, between-horizontal-start, between-vertical-start, bold, bolt, book-marked, book-open, book-text, bot, box, brackets, brain, briefcase, building-2, calculator, calendar, calendar-clock, calendar-off, camera, car, chart-column, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down-up, chevrons-up-down, circle-alert, circle-check, circle-x, clipboard-list, clipboard-paste, clock, code, code-xml, columns-3, columns-3-cog, combine, construction, container, copy, credit-card, database, decimals-arrow-left, decimals-arrow-right, dollar-sign, download, ellipsis, eraser, expand, external-link, eye, eye-off, facebook, file, file-csv, file-down, file-question, file-spreadsheet, file-stack, file-text, file-up, folder, folder-closed, folder-open, folder-pen, form, funnel-plus, funnel-x, gauge, globe, gpu, grid-2x2, grip-vertical, group, hand-coins, hash, heading, heading-1, heading-2, heading-3, history, house, image, inbox, info, instagram, italic, keyboard, languages, layout-dashboard, layout-grid, library-big, link-2, link-2-off, linkedin, list, list-checks, list-collapse, list-filter, list-filter-plus, list-ordered, list-tree, loader, lock, lock-keyhole, lock-keyhole-open, lock-open, log-in, log-out, mail, map-pin, maximize-2, megaphone, menu, message-circle, message-circle-question-mark, message-square, messages-square, mic, minimize-2, minus, monitor, mouse, mouse-pointer-click, music, newspaper, notepad-text-dashed, package, paint-bucket, paintbrush-vertical, palette, panel-left, panel-left-close, panel-left-open, panel-right, panel-right-close, panel-right-open, paperclip, pause, pencil, percent, phone, pin, pin-off, plane, play, plug, plus, receipt, rectangle-ellipsis, redo, refresh-cw, repeat, rotate-ccw, rotate-cw, rows-3, scan, scan-line, scissors, search, send, settings, share, share-2, shield, shield-alert, shield-check, shopping-cart, sigma, sliders-horizontal, smile, smile-plus, snowflake, sparkles, split, square, square-check, square-pen, square-sigma, stethoscope, sticky-note, strikethrough, table, table-2, tag, target, text-quote, text-wrap, thumbs-down, thumbs-up, ticket, trash, trending-down, trending-up, triangle-alert, truck, tv-minimal, twitter, type, underline, undo, upload, user, user-check, user-pen, users, utensils, waypoints, workflow, wrench, x, zap, zoom-in, zoom-out\n\n<!-- generated:end field-select-glyphs -->\n\n### `member`\n\nA person picker over the workspace\'s members. No default. With no role it is still drawn \u2014\nface and name, ranked as a `party` \u2014 in a screen\'s `columns` and in the register\na record draws of these rows.\n\n<!-- generated:start field-member -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-member -->\n\n### `link`\n\n<!-- generated:start field-link -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 or, reading on through one-row links, off the row the last of them names \u2014 `<link alias>.<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n| `options_where` | [filter](#views) | no | Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET table\'s own fields, each `field_key` a field alias. It narrows every picker of the link, and a save naming a row outside it is refused on every path \u2014 an app, the grid, an import, a workflow, the API. |\n| `same` | list of alias (at least one) | no | One-row links this row and each row this link points at must name alike \u2014 a fee\'s invoice is one of its own visit\'s, a box\'s seal one of its own shipping line\'s: each a one-row link of both tables to the same table. It narrows the picker\'s read to the rows naming what this row names, a row made from the picker names it too (set, never asked), and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare. |\n| `table` | text | yes | The table the link points at: its alias, or the `ref` of another item of the call |\n| `multi` | boolean | yes | true: the link holds many rows; false: one. Where the link is paired, at most one of the two sides holds one |\n| `pair` | `true` \\| alias | no | Makes the link two-way: the alias of its partner field on the other table, which names this one back, or `true` to have the alias derived. Stated on one table, the partner is made on the other with the inverse cardinality; a many-to-many link states both sides |\n\n<!-- generated:end field-link -->\n\n**`multi` is stated on every link:** `true` holds several rows, `false` one \u2014 one customer on an\ninvoice, one project on a device. There is no default.\n\nA two-way link states `pair` on one table, as `true` or a free alias, and the partner is made on the\nother table with the inverse cardinality; a many-to-many link states both sides, since the inverse of\nmany is not decided. A pair stated on both sides is made once. Both tables are written in the one\ncall (`table` names the other item\'s `ref`). A link with no `pair` is one-way, and nothing is created on the target. Pair a link when\nthe target\'s own record should list what points at it, and leave it unpaired when it should not: either\nway the record plan draws the relation as a section on the side it points at, so an unpaired link costs\nthe target nothing. Of two paired links at most one holds one row.\n\n```jsonc\n// a field of table "order"\n"customer": { "type": "link", "label": "Customer", "table": "customer", "multi": false, "pair": "orders" }\n```\n\n```jsonc\n// a field of table "customer"\n"orders": { "type": "link", "label": "Orders", "table": "order", "multi": true, "pair": "customer" }\n```\n\n### `files`\n\nA row attaches documents to it (\xA7 Rows). Each file\nis drawn by its kind, with no key to choose: a picture (an image, a video) as its\nthumbnail, a document (a PDF, a sheet) as its type\'s badge and its filename.\n\n### `formula`\n\nA formula states its keys on the field itself, beside `type` and `label`:\n`{ "type": "formula", "label": "Total", "expression": "{quantity} * {unit_price}", "notation": { "style": "currency", "currency": "USD" } }`.\n\n<!-- generated:start field-formula -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `expression` | text | yes | The expression, over fields of THIS entity by alias in braces \u2014 `{quantity} * {unit_price}` |\n| `output_type` | `"number"` \\| `"text"` \\| `"date"` \\| `"datetime"` \\| `"boolean"` \\| `"select"` | no | Absent, it is what the expression yields. Stated, it is checked against that and refused where they differ (a datetime is never stated date) |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. For a formula yielding a number. |\n| `link` | `true` | no | The formula yields a URL as text, drawn as a link that opens. |\n| `options` | list of [Select option](#select-option) (at least one) | no | The categories the formula yields, drawn as a single select\'s options are (read-only). The expression yields one of them as `{this_field:option}`, or null \u2014 `{days_idle} > 30 ? {warmth:cold} : {warmth:hot}`. Omit for a formula yielding a plain value. |\n\n<!-- generated:end field-formula -->\n\n`output_type` is what lets a role or a screen clause accept a computed value: a\ncaption over a derived name (`output_type: "text"`), a period over a settled date\n(`"date"`). A formula declaring no `output_type`, `notation` or `link` says nothing\nabout its result, and every rule that needs one refuses it by name. A formula stating\n`options` is a computed category: read-only, and read as a single select\nwherever it is drawn \u2014 a column, a filter, a reading\'s `by` or `where`.\n\n**A select reaches a formula as the KEYS of its chosen options**, a list \u2014\nnever their labels, and never their aliases \u2014 and a document has no keys: the\nworkspace mints them when the table is made. So an option is named in a formula\nas `{field:option}`, both aliases, and the write puts that option\'s key in its\nplace: `includes({kind}, {kind:crate})` for a select holding one or several,\n`{kind}[0] == {kind:crate}` for a single one. A select compared to its own words\n(`{kind} != "Crate"`) matches no row and computes the other branch everywhere,\nso the check refuses it and names the token. A select looked up from another\ntable is tested there, in a formula of its own, and that result looked up.\n\n**The language.** The check refuses a call or a name the platform cannot run, naming what to write:\n\n<!-- generated:start field-formula-language -->\n\n- Operators: `+ - * / %`, `== != > < >= <=`, `&& || !`; `+` also joins text\n- Numbers: arithmetic is exact decimal (`0.1 * 3` is `0.3`), a division that does not end the nearest double; a comparison between two numbers reads both at 15 significant digits; `round` rounds a half away from zero (`round(-2.5)` is `-3`)\n- Conditionals: a ternary only \u2014 `{amount} > 100 ? "High" : "Low"`\n- Not supported: optional chaining (`?.`), nullish coalescing (`??`), template literals, arrow functions \u2014 use `get(obj, "path", default)`, `coalesce(v1, v2)`\n- Helpers are these names, spelled exactly; a spreadsheet function (`IF`, `SUM`, `LEN`, `DATEDIF`) is none of them, and a formula calling one is refused\n- Math: round(n,decimals?), ceil(n), floor(n), abs(n), min(a,b), max(a,b), sum(arr), mean(arr), clamp(n,min,max), percentage(part,total,decimals?), pow(base,exp), sqrt(n), mod(n,divisor)\n- Strings: upper(s), lower(s), trim(s), capitalize(s), length(s), contains(s,search), join(arr,sep), split(s,sep), replace(s,search,rep), replaceAll(s,search,rep), removeChars(s,chars) (s without any character that occurs in chars), removeSpaces(s) (s without any whitespace), startsWith(s,prefix), endsWith(s,suffix), substring(s,start,end?), padStart(s,len,char), padEnd(s,len,char), numberToWords(n, lang?) (lang \'vi\' default, or \'en\'), uuidFrom(s) (the same UUID for the same text)\n- Lists: includes(list,value), first(list), last(list), unique(list), compact(list) \u2014 length(list) counts one\n- Dates: now(), formatDate(d,fmt), addDays(d,n), subDays(d,n), addHours(d,n), subHours(d,n), addMinutes(d,n), subMinutes(d,n), startOfDay(d), endOfDay(d), differenceInCalendarDays(later,earlier), differenceInHours(later,earlier), differenceInMinutes(later,earlier), isBefore(d1,d2), isAfter(d1,d2), isSameDay(d1,d2), isToday(d), isWithinRange(d,start,end), meetsBounds(value,kind,{gt,gte,lt,lte},tz?), parseDate(d)\n- Null/type: isNull(v), isEmpty(v) (also true for "" and []), coalesce(v1,v2,...), isString(v), isNumber(v), isBoolean(v), isArray(v), toNumber(v), toFigure(v), toString(v)\n- Other: formatCurrency(amount,locale,currency), formatDecimal(value,decimals,locale) (grouped quantity, no symbol), get(obj,"path",default?)\n- Empty cells: a cell nobody filled is null inside a formula, whatever its type; one holding 0, false or "0" is not empty. Test it with `isEmpty({note})` \u2014 `{note} == ""` and `{done} == false` are false on an unset cell\n- Arithmetic over an empty cell: `+` and `-` read it as 0 beside a value (`{fee} + {surcharge}` is `{fee}` when the surcharge is empty, null when both are); `*`, `/`, `%` and a unary `-` yield null (`{price} * {qty}` is null, not 0, when the quantity is empty)\n- No helper throws on an empty cell: the math helpers and toNumber return null, the string helpers "". A value of the wrong type still errors. When EVERY field a formula reads is empty it is null \u2014 unless it reads each only as the argument of isEmpty, isNull or isNotNull (`!isEmpty({file})` is false there, not null)\n\n<!-- generated:end field-formula-language -->\n\n### `rollup`\n\nAggregates the records reached through a link on this table.\n\n<!-- generated:start field-rollup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `filter` | [filter](#views) group | no | Only linked records matching this filter are aggregated; one condition on its own is a group of one. Every `field_key` in it names a field alias on the linked entity, and a select condition\'s value names an option alias there. A traversal node reaches past that entity, so its `path` hops and inner `field_key` are fully-qualified `entity.field` aliases. |\n| `via` | alias | yes | The alias of a link of this table to roll up from |\n| `op` | [operation](#rollup) | yes | The operation over the linked rows |\n| `field` | alias | no | The alias of the field on the linked table the operation reads; absent for `count` |\n\n<!-- generated:end field-rollup -->\n\nA rollup states `via` (a link of this table), `op` and \u2014 except for `count` \u2014 `field`, an alias on the\nlinked table; `op` is one of `count`,\n`sum`, `avg`, `median`, `min`, `max`, `range`, `empty`, `filled`,\n`percent_empty`, `percent_filled`, `unique`, `percent_unique`, `earliest`,\n`latest`, `date_range`, `checked`, `unchecked`, `percent_checked`,\n`percent_unchecked`. The operation must be one the aggregated field\'s type\nallows \u2014 `sum` over a number, `earliest` over a date, `filled` over any stored or\nformula field. A lookup is never rolled up: roll up the child\'s own field, or a formula\nover it. A rollup states no `notation`: `sum`, `avg`, `median`, `min`, `max` and `range`\nread in the aggregated figure\'s, a count as a plain number, and a `percent_*` as a\npercent (0\u2013100), in the app as in the table.\n\n### `lookup`\n\nDisplays a field from the linked records: `via` names a link of this table, `field` a field of the table it\npoints at, and `order_by` (`field`, `direction`) picks the one row whose value shows.\n\n<!-- generated:start field-lookup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `via` | alias | yes | The alias of a link of this table to look up through |\n| `field` | alias | yes | The alias of the field on the linked table to show |\n| `order_by` | [Lookup order](#lookup-order) | no | Show ONE linked row\'s value \u2014 the first in this order \u2014 rather than every linked row\'s |\n\n#### Lookup order\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A field alias on the linked table the rows are ordered by |\n| `direction` | `"asc"` \\| `"desc"` | yes | Which end of that order the one row is taken from |\n\n<!-- generated:end field-lookup -->\n\nInside a formula, a lookup holding one value is that value (`{due_soon}` is\n`true`, not `[true]`); several values are a list.\n\n### `autonumber`\n\n<!-- generated:start field-autonumber -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `prefix` | text | no | Literal prefix prepended to every display value (e.g. \'KH-\' \u2192 \'KH-001\'). Ignored when `template` is set. |\n| `padding` | integer | no | Zero-pad the integer to this width. Default 1 (no padding). 3 \u2192 \'001\', \'012\', \'123\', \'1234\' (overflow uses the actual width). Ignored when `template` is set. |\n| `template` | text | no | Format template with placeholder tokens evaluated at insert time. Tokens: {N} (raw integer), {N:W} (zero-padded to width W, e.g. {N:3} \u2192 001), {YEAR} (4-digit year), {YEAR:2} (2-digit year), {MONTH} (2-digit month), {DAY} (2-digit day). Date tokens use the workspace timezone. Example: \'HM-{YEAR}-{N:3}\' yields \'HM-2026-001\'. Stored as the composed string; subsequent template edits do NOT re-format existing rows (date tokens would lose the original creation date). |\n| `next` | integer | no | Sets the number the next row takes, once, as the patch is written; never below one past the highest number issued. On a field added to a table with rows, those rows are numbered from it in creation order. It is no setting of the field: a read of the table shows none |\n\n<!-- generated:end field-autonumber -->\n\nA refused write and a dry run take no number, so a table\'s numbers run without a gap; a removed row\'s number is not\nissued again.\n\n<!-- generated:start rules-field -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `field.default` | A default names options its select declares \u2014 one on a single select \u2014 and a date stamped by `derive_from` states none. |\n| `field.default-from` | A field\'s `default_from` reads `<link>.<field>` off the row a one-row link of its table names \u2014 a link no copy fills \u2014 or, through further one-row links, `<link>.<link>.<field>` off the row the last names, into a field of the same type, date format and notation that `derive_from` does not stamp: a lookup only through a one-row link, a link only into one to the same table, a select\'s option as this field\'s of the same alias (one it lacks is not copied), several values only into a field holding several. The server copies it on every add that fills the link and leaves the field empty, so a List\'s `create` sure to fill the link \u2014 a required one, or the record it files the row under \u2014 does not ask it by default. |\n| `field.option-mark` | An option\'s `mark` is one the kit draws, and a select\'s options mark every one or none. |\n| `field.unit-select` | A notation\'s `per_row` names a single select of the same row, or a lookup of one through a one-link, whose every option label is a unit, or an ISO 4217 code. |\n| `field.link` | A link targets a table of the call or the workspace: its `pair` a link on that table naming this one back \u2014 and of two paired links at most one reads one row (`multi: false`). |\n| `field.formula` | A formula parses, reads fields of its own table by alias, names an option as `{field:option}` of one a select declares, and compares a select to its options\' keys, never their words. |\n| `field.formula-type` | A formula yields what it states \u2014 its `output_type` (the type the platform stores: a `datetime` is never stated `date`), else a figure where it states a `notation` and text where it states `link` \u2014 as the platform infers it from the expression when the field is created. |\n| `field.rollup` | A rollup aggregates, through a link of its table (`via`), a field of the linked table (`field`) by an operation (`op`) that field\'s type takes \u2014 never a figure read in each row\'s own unit or currency \u2014 filtered on fields of the linked table. |\n| `field.lookup` | A lookup reads (`field`), through a link of its table (`via`), a field of the linked table \u2014 a figure read in each row\'s own unit or currency only where this table looks up that row\'s select through the same one-row link \u2014 ordered by a field of it (`order_by`). |\n| `field.computed-cycle` | Computed fields never wait on each other in a loop: each is computed after what it reads. |\n\n<!-- generated:end rules-field -->\n\n## Views\n\nSaved views live under the table they belong to. Every field reference is a\nfield ALIAS on that table. A `set_tables` patch of an existing table refuses `views`: they are stated when the table is\ncreated, and change afterwards in chat.\n\n<!-- generated:start views -->\n\n#### View\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable view alias, unique within the entity |\n| `label` | text | yes | Display name of the view |\n| `description` | text | no | The view\'s description, written onto the view in the workspace |\n| `columns` | list of [View column](#view-column) (at least one) | no | The columns the view shows, in this order and no others; absent, every field |\n| `filters` | [filter](#views) | no | The rows the view keeps |\n| `sort` | [sort](#views) | no | The order the view reads its rows in |\n| `summary` | map of text \u2192 text | no | Field alias \u2192 the operation its footer cell states |\n| `frozen_columns` | integer \\| `null` | no | How many leading columns stay in place while the rest scroll |\n\n#### View column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_alias` | alias | yes | A field of this entity |\n| `visibility` | `"visible"` \\| `"hidden"` | yes | Field visibility state: \'visible\' = shown to everyone, \'hidden\' = not shown by default but members can toggle |\n| `width` | number | no | The column\'s width, in pixels |\n\n<!-- generated:end views -->\n\nA filter is a group \u2014 `{ "node_type": "group", "logic": "and" | "or",\n"children": [ \u2026 ] }` \u2014 or one condition on its own, `{ "node_type":\n"condition", "type": "select", "field_key": "tier", "operator": "has_any_of",\n"value": ["gold"] }`. A sort is a list of `{ "field_key": \u2026, "order": "asc" |\n"desc" | null }`. A condition\'s `type` is the field\'s type and its `operator` is\none that type admits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n<!-- generated:start rules-view -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `view.fields` | A view\'s columns, summary, sort and filters name fields of its table. |\n\n<!-- generated:end rules-view -->\n\n## Groups and templates\n\nA member group and a document template are made on their own, outside a table document and an app tree. A tree names a\ntemplate by its alias. A table document names a group by its id, where a `read_scope` clause (`member_of`),\na stage owner (`group`) or a check\'s `member_of("<id>")` takes one. `get_workspace` kind `group` lists the groups with their ids and members; `set_group` creates,\nchanges or deletes one (an admin\'s verb).\n\n`{{name}}` in an `html` or `email` template is filled from the workflow\'s data. Inline content is refused where it holds a\nscript, a frame, a `<link>`, an inline event handler or a `javascript:` URL; an `html` one may be a fragment, kept as\nwritten and printed as a whole document.\n\nA template\'s markup is the `template` of `set_template`, as one JSON string. A paper shaped like a counterparty\'s \u2014 a letterhead, a reference line, a seal, a signature block \u2014 is an `html` template with a shell around the body; `document_templates` covers the types.\n\n**What an act\'s template is handed** is its record\'s own fields by alias: a\nselect, a member and a link as their words (a link as the linked row\'s title), a\nnumber, a date and a computed value that declares its kind raw (`2800000`,\n`2026-09-14`) and a files field as its file cells (`{id, name, mime, url}`, the photos in the order held; a child row\'s\nfiles likewise) \u2014 never a computed select. On one record it is\nhanded, too, the rows of each child List the record draws, listed under that child\'s\nlink\'s name for `{{#each <link>}}` (and under the child entity\'s alias too): every row filed under the record, oldest first, each\nby its own fields as the record\'s are, less its link back to the record. Over\nseveral rows the rows are listed under the table\'s alias, for\n`{{#each <alias>}}`, without their child rows.\n\n**Photos in a paper.** A file cell is drawn only if whoever makes the paper may read that file, by the rule the file\nproxy applies; a file they may not read is never drawn. html: `{{#each photos}}<img src="{{url}}">{{/each}}`, each\nphoto inline, only those the data\'s file cells carry (a url typed into a text field stays a link). Word:\n`{{IMAGE picture(cover, 8)}}` for one file cell, and `{{FOR p IN photos}}{{IMAGE picture($p, 8)}}{{END-FOR p}}` for\nmany. Excel: `{{cover | picture:6}}` as the whole cell, and `{{#each photos}}` with `{{this | picture}}` for many, each\nrow growing to its picture. The number is the width in centimetres (8 by default, 6 in Excel); the height follows the\npicture. `picture` takes one image file cell: a list, a file that is no image and a file that cannot be read are\nrefused by name. Photos are embedded at most 1600 pixels across.\n\nHelpers print a raw value in the template\'s `locale` \u2014 `vi` or `en`, the organization\'s language where it states\nnone; each prints `""` for an empty value:\n\n| Helper | Prints (`vi` / `en`) |\n|---|---|\n| `{{money total "USD"}}` | `1.250,50 US$` / `$1,250.50`, in the currency\'s minor units; no code, a number |\n| `{{number qty}}` | `1.234,5` / `1,234.5`: grouped, up to 2 decimals |\n| `{{date issued_on}}` | `14/09/2026`; a date-fns pattern as the second argument, `{{date at "dd/MM/yyyy HH:mm"}}`, literal text quoted (`"\'Ng\xE0y\' dd"`). A moment prints on the workspace\'s timezone |\n| `{{words total "USD"}}` | `M\u1ED9t ngh\xECn hai tr\u0103m n\u0103m m\u01B0\u01A1i \u0111\xF4 la M\u1EF9 n\u0103m m\u01B0\u01A1i xu` / `One thousand two hundred fifty US dollars and fifty cents`; no code, VND (`\u0111\u1ED3ng`) |\n| `{{qr link}}`, `{{code128 position 6}}` | the value as a QR code, or as a Code 128 barcode (ASCII text only), a picture the width of the optional second argument in centimetres, by default sized to the value (a module prints 0.5 mm in a QR code, 0.33 mm in a barcode); a width too narrow to scan is refused, naming the least that does; not in an email template |\n| `{{date (add_days issued_on -1)}}` | `13/09/2026`: the date shifted by whole days (`-1`: the day before), for `date` to print; a number field as the days, empty shifting none |\n\nA value it cannot read (`{{money "abc"}}`, `{{date "14/09/2026"}}`) fails the render. A field named like a\nhelper and written alone (`{{number}}`) prints its own value.\n\nA field printed as a code in the other types: Excel `{{position | qr}}` or `{{position | code128:6}}` as the whole\ncell and the last helper \u2014 the picture is anchored on the cell and its row grows to hold it; Word `{{IMAGE qr(position)}}` or `{{IMAGE code128(position, 6)}}`. An empty field draws no picture.\n\n## Rows\n\n`rows` of a `set_tables` call are the first records of the tables the call creates, keyed by each one\'s `ref`;\na patch of an existing table seeds none, so write its rows into the table. A sample, not a data set: rows past it go in with `create_records` once the tables exist, in the batches its description bounds.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n`rows` sits beside `tables` in the call, not inside a table.\n\n<!-- generated:start rows -->\n\n#### Row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | text | yes | Local handle for this row, referenced by other rows\' link fields |\n| `fields` | map of text \u2192 any value | yes | Field alias \u2192 the value, read against the field\'s declared type |\n\n<!-- generated:end rows -->\n\n<!-- generated:start rules-rows -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `rows.ref` | A row\'s `ref` is unique within its table. |\n| `rows.required` | A row states every field its table requires: it is created with what it states, its fields\' defaults and copies unapplied. |\n| `rows.value` | A row\'s value fits its field: an option alias for a select, `"<table ref>:<row ref>"` of a row of the linked table in this call for a link, `"self"` for a member, a `fil_` id for files, a date in the date grammar (an hour only on a datetime), a text, number, yes/no or null otherwise \u2014 and none on a computed field. |\n\n<!-- generated:end rules-rows -->\n\nA `ref` is lowercase letters, digits and underscores, and is never persisted.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `link` | `"<table ref>:<row ref>"` naming another row of this call \u2014 `"customer:acme"`, or an array for several; a paired link is stated on ONE side (the child\'s link to its parent) and its partner fills itself |\n| `member` | `"self"` only \u2014 the member making the call |\n| `files` | the `fil_` id of a file already in this workspace, or a list of them |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe call is made, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n- on a date that holds its hour (`format: "datetime"`), that hour after the day, and always stated:\n `@today 14:30`, `@today+1 06:00`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Records\n\n`records` says how a row of a table is RECOGNISED \u2014 stated once in the table\'s document, beside its\n`title` \u2014 and every surface that draws one of its rows reads the same\nstatement: the register\'s row, a picker\'s option, a link\'s chip, a child table\'s\nrow and the record\'s header. Every table an app lists, opens or picks has one. The rarer keys \u2014 `scan`, `cadence`, `place`, `on_site`, `pin`, `plan_place` and `size` \u2014 are stated on `reference/record-places-pins-and-cadence`.\n\n```jsonc\n// table "visit"\n"title": "container_no",\n"records": {\n "subtitle": ["customer", "arrived_on"],\n "image": "photos",\n "status": { "field": "stage", "closed": ["gone"], "history": "visit_history" },\n "figure": "total_fees",\n "due": ["free_until"]\n}\n```\n\n```jsonc\n// table "release": a release order read against what it allows \u2014 a meter wherever it shows\n"title": "release_no",\n"records": { "figure": "issued", "limits": { "issued": "units" } }\n```\n\n```jsonc\n// table "dossier": a straight-line process \u2014 the stage a select, each step\'s day the date it stamps\n"title": "applicant",\n"records": { "status": { "field": "stage", "closed": ["signed"], "dates": { "received": "received_on", "appraised": "appraised_on", "signed": "signed_on" } } }\n```\n\n<!-- generated:start records -->\n\n#### Record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title wherever a row is drawn (a code, a date, a party): one line at a desk, wrapping to a second on a phone |\n| `image` | alias | no | A files field that pictures the row \u2014 never one an act keeps the document it makes in |\n| `mark` | alias | no | A single select whose option leads the row wherever a mark leads it \u2014 a list\'s row, the record\'s head, a link naming the row: the option\'s brand or glyph, else its colour dot, on a tile. The field stays a field: a subtitle run, a column, a section\'s control. Never beside `image` or `party`, and never the status field |\n| `description` | alias | no | A text field saying what the row is about \u2014 a plan\'s brief, a customer\'s note: read in its record\'s head under the title\'s line, its first lines at rest and whole on a press, and written from the head\'s \u270E. Never on a row\'s line |\n| `party` | `"person"` \\| `"organization"` | no | What each row is where the rows are people (a contact, a patient) or organisations (a customer, a supplier, a carrier); absent, the rows are things (an order, an item, a paper) |\n| `status` | [Status](#status) | no | The stage a row moves through, a single select, drawn as a badge beside its title |\n| `figure` | alias | no | The one number the row stands for, read at its right (a total, a quantity left) |\n| `limits` | map of alias \u2192 (alias \\| number) | no | A number read against a bound \u2014 a field of the same row or a constant \u2014 drawn as a meter wherever it shows |\n| `gates` | map of alias \u2192 (alias \\| number) | no | A pass mark on a meter `limits` bounds \u2014 a field of the same row or a constant; a percent field is its share of the bound, any other number an amount. The meter marks it and fills complete once past it |\n| `tolerance` | map of alias \u2192 number | no | How far past its `limits` bound a meter may stand, in percent of the bound: the bound is an amount expected (received against ordered), not a cap. Without one the bound is a cap, and past it is an overrun |\n| `due` | list of alias (at least one) | no | Dates that are deadlines (stored, or computed as a date): each counts down and turns overdue |\n| `frees` | alias | no | A date ending the row\'s span of days on which what the row holds is free again (a check-out, a hire\'s return): a stay from the 3rd to the 5th holds the 3rd and the 4th, and another may start on the 5th \u2014 on a lanes board, a calendar, a roster and in `no_overlap` alike. Absent, a span of days holds its last day; a span of moments always frees its end |\n| `settled` | alias | no | A yes/no of the row (stored, a formula: an invoice paid in full, or looked up through a one-row link: a line settled with its parent) that ends its deadlines: while it is true, no `due` of the row counts down or turns overdue \u2014 in a cell, on its line, in its head, on a calendar or gantt, or in the overdue count. A dashboard\'s or reading\'s `where` reads it as any other yes/no |\n| `task` | `true` | no | A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record\'s rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed` |\n| `applies` | map of alias \u2192 map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Fields that apply only while their conditions hold (a length only on a line whose tariff charges by the metre): where one does not apply no add asks it and no surface shows it. Never the title; a required one starts at its `default` |\n| `starts` | map of alias \u2192 `"today"` \\| `"me"` | no | What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default` |\n\n<!-- generated:end records -->\n\n<!-- generated:start rules-records -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.title` | A title is the one field naming the row \u2014 any field but a button or a yes/no \u2014 and the subtitle never names the title\'s own field. |\n| `records.image` | `image` is a files field, never one an Act keeps the documents it makes in (`into`). |\n| `records.mark` | `mark` is a single select, never the status field, and never beside `image` or `party` \u2014 a List keeps one lead slot \u2014 nor on the folder an App works inside (`scope`), which its switcher names by a picture or its initials. |\n| `records.description` | `description` is a text field, read in the record\'s head \u2014 never a Field of a part too. |\n| `records.figure` | A figure is a number. |\n| `records.figure-on-line` | *Noted, never refused:* A subtitle entry equal to the figure is shown once, as the headline; the line leaves it out. |\n| `records.applies` | `applies` holds a field of the row other than its title to conditions of the same row \u2014 a yes/no (stored or a formula) by `true` or `false`, a single select by its options, its own or looked up through a one-row link \u2014 never to itself; a required field it holds starts at a `default`. |\n| `records.limits` | `limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit. |\n| `records.gates` | `gates` marks a pass on a meter `limits` bounds: a percent field (its share of the bound), another number in the bound\'s unit, or a constant. |\n| `records.tolerance` | `tolerance` stands on a meter `limits` bounds. |\n| `records.starts` | `starts` starts a field a person writes \u2014 `today` a date or a moment (never a span), `me` a member \u2014 and a `create` that does not ask a field it starts writes that start. |\n| `records.due` | A deadline (`due`) is a date. |\n| `records.frees` | `frees` names a stored day, never a moment: a span of moments already frees its end. |\n| `records.settled` | `settled` names a yes/no of the row (stored, a formula, or looked up through a one-row link) beside a `due`: while it holds, the row\'s deadlines neither count down nor turn overdue. |\n| `records.task` | A task entity (`task`) states a `status` of a stored single select with `closed` options \u2014 its ring ticks a row done by moving it to one. |\n| `records.picture` | *Noted by the designer on a write, never refused:* Rows an App draws that each hold one files field no Act makes documents into, no call is filed into, no files gallery keeps and no List compares as a pair (`compare`) \u2014 and no figure \u2014 are pictured by it (`image`); where the workspace\'s files are known, the field holding images is the picture, a figure or not. |\n\n<!-- generated:end rules-records -->\n\n- **The title names the row** by who or what it is \u2014 a link by the linked row (a job\n read by its customer). Any field but a button or a yes/no may title it, yet a code\n titles a row only where people name the row by it, else it is a `subtitle`; it is the table\'s own setting, which every link shows.\n- **One lead per row.** A row leads with its picture (`image`), a face (`party`) or the\n option of a single select it holds (`mark`) \u2014 never two. A `mark` draws that option\n on a tile: its brand or glyph, else its colour dot; a row holding none, a ring saying\n none is picked. The same tile heads the row\'s record and leads it where a link\n names it; the select stays a field \u2014 a subtitle run, a column, a section\'s control.\n- **A limit** reads a number against a bound \u2014 another field of the row, or a constant.\n A `gates` entry marks a pass on that meter: a percent field is its share of the\n bound, any other number an amount.\n- **A deadline** (`due`) counts down wherever it is drawn and is overdue once past while\n the work is open. A register over the table says how many rows in view are overdue on\n its summary line, and pressing that count narrows to them \u2014 no reading to state.\n- **A span\'s free day** (`frees`) is the end date on which what the row held is free\n again \u2014 a check-out, a hire\'s return. A stay from the 3rd to the 5th holds two nights:\n a lanes board, a calendar and a roster draw it through the 4th, and `no_overlap` lets\n the next stay start on the 5th. Without it a span of days holds its last day; a span of\n moments always ends as the next may begin.\n- **`starts`** is where an add\'s field begins, stated only where that is certain \u2014 a\n log\'s own day at `today`, a request\'s requester at `me`. Nothing else starts a field\n but its declared `default` and what the reader narrowed the register to.\n\n## Record places, pins and cadence\n\nThe rarer keys of `records`, stated beside the ones on `reference/records`: a title read off a photo (`scan`), the rows a child owes each day (`cadence`), where a row stands on a map (`place`, `on_site`) or on a picture (`pin`, `plan_place`).\n\n```jsonc\n// table "site": it owes a diary page each day it works, from breaking ground through handover\n"title": "name",\n"records": { "cadence": [{ "entity": "diary_page", "on": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "started", "until": "handed_over" }] }\n```\n\n<!-- generated:start records-places -->\n\n#### Record places\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `cadence` | list of [Cadence](#cadence) (at least one) | no | The rows of a child each row owes one of a day, on the days of the week listed (a site\'s diary page, Monday to Saturday) \u2014 one entry a child, never owed on a day still to come. A roster of these rows over days marks a past day owed with none missing in the danger tone, counted in the legend; today\'s is due, offered to add and never marked; each row\'s total reads the days it holds of those owed |\n| `scan` | `"text"` \\| `"iso6346"` \\| `"code"` | no | Its title, a text field, is painted or printed on the thing a row is (a container\'s number, a seal\'s) and read off one photo on a phone, beside each search matching it and each field holding or picking it: `iso6346` a container number, refused unless its check digit holds; `text` as it stands; `code` a barcode or QR code on the thing, decoded on the device from the photo with no agent |\n| `place` | [Place](#place) | no | Where a row stands, as plain fields of it, drawn as one place where its record draws the address: how sure it is, a press opening it in the device\'s maps, and one taking it from where the device stands |\n| `plan_place` | [Plan place](#plan-place) | no | Each row is a place on the plan a `plan` register draws: its title the place\'s code, its subtitle a note. A flat list, no tree |\n| `on_site` | [On site](#on-site) | no | Photos taken where a place stands (a site diary\'s): a photo taken through a phone\'s camera here keeps where the device stood, marked at the place, away from it or unknown, and a record holding one taken away says so among its checks \u2014 one with no place (picked on a computer, or kept before places were) is marked and warns nothing; a place reaches only members of the organization, through the apps drawing these photos |\n| `pin` | [Pin](#pin) | no | Each row is a place on a picture (a snag on a flat\'s plan, a spot on a container\'s drawing): a List of these rows draws the picture its `on` row holds with a numbered pin at each row\'s own place, each row led by its number, the row opened lit on it and a pin pressed opening its row; where the List writes rows, a press on the bare picture adds one there, and a pin dragged moves its row. A List of rows that link to these by a single link draws their places the same way |\n| `size` | [Record size](#record-size) | no | The real size of the thing a row is (a carton, a piece of furniture, a pallet): three number fields stating one length unit alike, read as one fact \u2014 "400 \xD7 300 \xD7 250 mm", a part the row lacks "\u2014" \u2014 after the subtitle wherever the row\'s line and head draw facts, never also as the subtitle\'s fields. A plan register drawing these rows draws each box at this size |\n\n#### Cadence\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | text | yes | The table owed: each of its rows stands on one row of this table by its single link to it, and on the first date of its line (`records` title or subtitle) |\n| `on` | list of `"mon"` \\| `"tue"` \\| `"wed"` \\| `"thu"` \\| `"fri"` \\| `"sat"` \\| `"sun"` (at least one) | yes | The days of the week one is owed on, each listed \u2014 `mon` to `sun` |\n| `from` | alias | no | A date of this row: the first day one is owed (a site breaking ground, a contract\'s start); absent, no first day |\n| `until` | alias | no | A date of this row: the last day one is owed (a site\'s handover, a contract\'s end); empty or absent, through today |\n\n#### Place\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `address` | alias | yes | A text field: the address, as people write it |\n| `latitude` | alias | yes | A number field: degrees north, negative south |\n| `longitude` | alias | yes | A number field: degrees east, negative west |\n| `accuracy` | alias | no | A number field: how many metres off the device that gave the place said it may be |\n\n#### On site\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photos` | list of alias (at least one) | yes | Files fields of this row holding photos taken on site |\n| `place` | alias | no | A single link of this row to the row whose `place` its photos are taken at (a diary page\'s site); absent, this row\'s own `place` |\n| `radius` | number | yes | How many metres from the place a photo still counts as taken there |\n\n#### Pin\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `on` | alias | yes | A single link of this row to the row whose picture (its `records` `image`) the place is on \u2014 a container\'s drawing, a floor\'s plan |\n| `x` | alias | yes | A number field of this row: how far across that picture the place stands, in percent from its left edge |\n| `y` | alias | yes | A number field of this row: how far down that picture the place stands, in percent from its top edge |\n\n#### Record size\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `across` | alias | yes | A number field: the width of the thing a row is, across |\n| `along` | alias | yes | A number field: its depth, along |\n| `up` | alias | yes | A number field: its height, up |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.scan` | A title read off a photo or a code (`scan`) is a text field the row stores. |\n| `records.cadence` | A `cadence` names each child once \u2014 its rows standing on one row of this entity by their single link and on the first date of their line \u2014 lists each day of the week once, and `from` and `until` are two different days of this entity. |\n| `records.place` | A `place` names four different fields of the row: its address a text field, its latitude, longitude and accuracy number fields it stores. |\n| `records.plan_place` | A `plan_place` names `kind` and `reach_from` as single selects, `sheet` as a single select or a one-row link, `stacked` as a boolean and the rest as numbers, each a different field; `kind`, `x`, `y` required. `size` names three numbers (`across`, `along`, `up`) stating one cell in one length `unit` alike. A `sheet` linking to an entity that states an `image` draws that drawing under the places, `scale` (a number field of that entity) its width in cells; `scale` goes with such a `sheet` alone. |\n| `records.on_site` | `on_site` lists files fields of the row, each once, taken at the `place` of the row itself or of the one row a single link of it names (`place`), which states one. |\n| `records.pin` | A `pin` names a single link of the row (`on`) to an entity whose `records` states its `image` (a PDF is drawn as its first page), and two different number fields of the row, how far across (`x`) and down (`y`) that picture in percent. A List of rows whose own entity states a `pin` is drawn over that picture; `on`, `x` and `y` are set by the press that adds a row, never asked. |\n| `records.size` | A `size` names three different number fields of the row (`across`, `along`, `up`) stating one length unit alike (mm, cm, m or km); they read as one fact after the subtitle, so the subtitle never also names one. |\n\n<!-- generated:end records-places -->\n\n## Status\n\nA record\'s `status` is the stage a row moves through \u2014 a stored single select, each\noption\'s moment the date `dates` names \u2014 drawn as a badge beside its title wherever the row is.\n\n<!-- generated:start status -->\n\n#### Status\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The single select that is the row\'s state, its options in the order the work goes through them |\n| `closed` | list of alias (at least one) | no | Options that end the work: a register opens on the rows not at them, and the badge reads muted |\n| `dropped` | list of alias (at least one) | no | Options of `closed` that leave the work undone (cancelled, rejected): whoever holds the row moves it to one from any stage, where every other move forward passes each owned stage between that binds the row (its `when`) and needs each one\'s owner, and a row leaving one enters a stage as a row made there does |\n| `history` | text | no | A table every write that sets or moves the status appends one row to (its link to this table, the new status, the moment and who) \u2014 by the record write path, whoever writes: for reporting on every move |\n| `dates` | map of alias \u2192 alias | no | Option alias \u2192 a date field of this entity the record write path stamps with the moment a row moves into that option, or is made at it \u2014 whoever writes. A value the same write states for that field wins, so past data loads as it happened; the platform never clears a stamp, so each is when the row last entered the option. A stage\'s date is made where none is named |\n| `stages` | map of alias \u2192 [Stage](#stage) | no | Option alias \u2192 who owns that stage and how long it should take: the owner alone takes a row out of it, by any write, and presses the acts offered there; every move forward past it of a row it binds (its `when`), and every such row made past it, is its owner\'s too. Its days run from the moment its `dates` field holds. In an app the status then moves only by acts that `set` one of its options \u2014 no save writes it and no act asks it |\n\n#### Stage\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `owner` | text \\| [Stage group](#stage-group) | yes | Who owns the stage: a member field of the row, `link.field` read through a one-row link (not at the stage a row opens at, whose owner made the row: a member field of the row itself), or `{ group }` |\n| `within` | integer | no | Whole days the stage should take from the moment the row entered it |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| [comparison](#values) \\| `{ "empty": true \\| false }`) | no | The rows that must pass this stage: a row meeting every entry on its own fields, as it stood before a write or as the write leaves it \u2014 a single select holding one of these options, a yes/no this value, a number or date meeting its bounds, a field empty or filled. Absent, every row must. A row at the stage leaves it by its owner either way |\n\n#### Stage group\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `group` | text | yes | A member group\'s id (from Settings, or chat): every member of it owns the stage |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.status` | A status is one single select, its options in the order the work goes through them, `closed` naming the ones that end it and `dropped` those of them reached from any stage. |\n| `records.history` | A declared status `history` has exactly one link to the row it records, one single select holding every option of the status (or a text), exactly one date and at most one member. |\n| `records.stages` | A stage\'s owner (`status.stages`) is a single member field of the row, one read through a one-row link (never at the stage a row opens at: its owner made the row, so it is a member field of the row itself), or a group, on an option that does not end the work; a group\'s stage needs the status `history` with who moved the row. Every write takes a row off a stage as its owner alone, and moves a row past it or makes a row past it as its owner alone where the stage binds the row (its `when`) \u2014 a move to a `dropped` option excepted, which whoever holds the row makes from any stage; a row leaving a dropped option, or holding none, enters a stage as one made there. Each stage\'s moment, and each open option\'s, is the date its `dates` names, made where none is named, which every write that opens or moves the status stamps; `within` runs from it. In an App a status with stages moves only by Acts that `set` one of its options \u2014 no save writes it or a date it stamps, and no Act asks either, sets it otherwise or writes either from a workflow; an Act moving a row forward past a stage another owner owns, into an option not `dropped`, keeps its `when` off every row that stage binds. |\n| `records.stage-when` | A stage\'s `when` names the rows that must pass it, as an Act\'s `when` holds them \u2014 single selects to their options, yes/nos to `true` or `false`, numbers and dates to bounds, any field to `{ "empty": true \\| false }` \u2014 on fields a write stores of the row itself: never the status, a formula, a lookup or a rollup, and never on the stage a row opens at. It binds a row meeting it as it stood before a write or as the write leaves it; absent, it binds every row. |\n| `records.detour-note` | *Noted, never refused:* A stage the work only steps aside into \u2014 not where a row starts, entered from one step alone that also moves the work on without it, and left by its own moves only back to that step (a pause, a repair) \u2014 is a detour: its option and the Section whose `at` holds it stand before that step\'s, else the work reads it as its next step, and the Act moving the work into it leads that Section \u2014 never a correction waiting in a \u22EF. |\n\n<!-- generated:end status -->\n\n- **The status** is one single select. `closed` options end the work: a register opens\n on the rows not in them. A `history` table gets one row per move \u2014 its link to this\n table, the new status (a select holding the same option aliases, or text), the\n moment and who \u2014 appended by the record write path in every write that opens a row at a\n stage or moves it, whoever writes (the member behind the write is its `by`; an automation\n leaves it empty). A `history` the tables do not declare is derived, and needs no\n `records` of its own.\n- **When each step happened** is the date `dates` names for its option, stamped as the row\n enters it, whoever moves it. One way times a status: a straight-line process is a select\n whose options are its steps in order, each with its date \u2014 never a run of dates the stage\n is read off. An Act that moves the stage and `asks` the step\'s date with `starts: "today"`\n lets the reader record a day already past in the same press.\n\n## Write rules\n\nWhat a WRITE of a table\'s rows meets is stated in the table\'s document, where it applies: `min` and\n`max` on a number or date field, `options_where` and `same` on a link, `suggest` on a text field,\nand `no_overlap` and `natural_key` on the table. A key a field\'s type does not take is refused at\nits path, and a document has no `write_rules` key.\nEvery one but `natural_key` and `suggest` holds on every path a record is written by \u2014 the grid, a record\'s form, an import, a workflow, an\nagent, the API, an app \u2014 judged in the write\'s own transaction, every broken rule listed at once; the\nscreen and a picker only mirror them. A row that already breaks one stays editable until a write\ntouches what the rule reads. `natural_key` names the fields an app recognises a row by, to reuse it where an\nadd or a find-or-create meets it; a record tool or the API creates the row it is handed. `suggest` offers\nvalues in a text control and refuses none. An `options_where` link is refused where a write names a row the\nnarrowing does not admit, and every picker of the link offers only the rows it admits.\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "label": "Customer",\n "fields": { "email": { "type": "text", "label": "Email", "unique": true } },\n "natural_key": ["email"]\n }\n ]\n}\n```\n\nA customer above is RECOGNISED by their address. An add that names one \u2014 from the customers\nregister or from a picker\'s "new" \u2014 reuses the row it matches and opens one only where nothing does,\nso the book never grows a second Acme.\n\n```json\n{\n "tables": [\n { "ref": "product", "label": "Product", "fields": { "name": { "type": "text", "label": "Name" }, "in_stock": { "type": "number", "label": "In stock" } } },\n {\n "ref": "order",\n "label": "Order",\n "fields": {\n "name": { "type": "text", "label": "Name" },\n "placed_on": { "type": "date", "label": "Placed on" },\n "due_on": { "type": "date", "label": "Due on" },\n "lines": { "type": "link", "label": "Lines", "table": "order_line", "multi": true, "pair": "order" }\n }\n },\n {\n "ref": "order_line",\n "label": "Order line",\n "fields": {\n "name": { "type": "text", "label": "Name" },\n "order": { "type": "link", "label": "Order", "table": "order", "multi": false, "pair": "lines" },\n "quantity": { "type": "number", "label": "Quantity", "min": 1, "max": 9999 },\n "ships_on": { "type": "date", "label": "Ships on", "min": "order.placed_on", "max": "order.due_on" },\n "product": {\n "type": "link",\n "label": "Product",\n "table": "product",\n "multi": false,\n "options_where": {\n "node_type": "group",\n "logic": "and",\n "children": [{ "node_type": "condition", "type": "number", "field_key": "in_stock", "operator": "greater_than", "value": 0 }]\n }\n }\n }\n }\n ]\n}\n```\n\nA line of nothing is not a line: `quantity` is refused on create and on update. A line ships inside\nits order\'s window \u2014 a bound read off the row a link names is judged when the line is written. Nothing is sold off an empty shelf: every picker of `product`\nreads only the rows that answer its `options_where`, and every save refuses the same rows again.\n\n```json\n{\n "tables": [\n {\n "ref": "booking",\n "label": "Booking",\n "fields": {\n "room": { "type": "text", "label": "Room" },\n "state": { "type": "select", "label": "State", "options": [{ "alias": "held", "label": "Held", "color": "blue" }, { "alias": "released", "label": "Released", "color": "gray" }] },\n "starts_on": { "type": "date", "label": "Starts on" },\n "ends_on": { "type": "date", "label": "Ends on", "min": "starts_on" }\n },\n "no_overlap": { "from": "starts_on", "to": "ends_on", "by": ["room"], "while": { "state": ["held"] } }\n }\n ]\n}\n```\n\nOne booking per room at a time, while it is held \u2014 a create or a save whose span overlaps another\nheld booking of the same room is refused.\n\nA table\'s `checks` (the conditions no write of its rows may meet) and `workflows` (what follows every committed write of them) are stated in its document and hold on every path a record is written by; their keys, what they read and an example are on `workflows/table-checks` and `workflows/table-workflows`.\n\n<!-- generated:start write-rules -->\n\n#### Suggest\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `table` | text | yes | The table whose text field is offered from: its id, or its `ref` in this call |\n| `field` | alias | yes | The alias of that table\'s stored text field; the field\'s control offers, as the reader types, the distinct values it holds, and any text is still written |\n\n#### No overlap\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | text | yes | The stored date (or datetime) a row\'s span starts on: `<field alias>` of the row, or `<link alias>.<field alias>` of the one row a one-row link names (a booking line\'s piece held for its booking\'s dates) |\n| `to` | text | yes | The stored date (or datetime) a row\'s span ends on, named like `from` and through the same link \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at |\n| `by` | list of alias (at least one) | no | Fields two rows share to compete for a span (the same employee, the same room); absent, every row |\n| `while` | map of text \u2192 list of alias (at least one) | no | Only rows at these options of these selects hold their span (running, not closed), the select named like `from` \u2014 `<field alias>` or `<link alias>.<field alias>`; absent, every row |\n\n<!-- generated:end write-rules -->\n\n<!-- generated:start rules-write -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `write.natural-key` | A `natural_key` is text or a number a person types back, or a one-row link; a text key is unique \u2014 `unique: true`, or the key\'s fields as a set in the table\'s `unique`. |\n| `write.bounds` | `min` and `max` bound a number or a date \u2014 by a constant a number, by a field of this row or of the one row a one-row link names either, of the same type and unit, never by itself \u2014 and `min` is never above `max`. |\n| `write.bound-per-row-unit` | On a figure whose unit or currency is the row\'s own (its notation\'s `per_row`) a constant bound is only 0 \u2014 bound it by a field of the row. |\n| `write.suggest` | `suggest` stands on a stored text field and names a `table` and a `field`, a stored text field of that table; the app reads that table, so it has a `records` entry. |\n| `write.same` | `same` stands on a link and names one-row links this table and the link\'s target both hold to one table. |\n| `write.options-where` | `options_where` narrows a link by an `and` group of plain conditions over the target\'s own fields \u2014 a number compared in one unit on every row, a select by options it declares. |\n| `write.no-overlap` | `no_overlap` spans two date fields, the row\'s own or the one row a one-row link names (`booking.start_on`); `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options, the row\'s own or that linked row\'s. |\n\n<!-- generated:end rules-write -->\n\n- A `natural_key` is `text` or `number`, because a person types it back, or a\n one-row link, because a row is also recognised by the one row it belongs with (a\n fee by its visit and its kind). A key holding a `text` field is unique: `unique:\n true` on that field, or the key\'s fields as a set in the table\'s `unique` (a\n site by its customer and its name) \u2014 two rows sharing it would make\n find-or-create pick whichever the read answered first. An add asks the key by\n default.\n- A `same` link names only rows that name what this row names \u2014 a fee\'s invoice\n is one of its own visit\'s \u2014 through a one-row link both tables hold to one\n table; its picker offers nothing until the row names one. A row added under a\n record names what it copies off it (`default_from`) from the start, so a block\n expecting one line per row of the link lists that record\'s rows alone. A save\n of this row moving what the two compare is refused while they would name apart.\n- An `options_where` link may be left empty; a row it names is refused on every\n save that writes the link unless it holds the narrowing, and the refusal names\n the link and the row. A save that leaves the link alone is not judged, so a row\n still naming a row the narrowing no longer admits stays editable. A link naming\n one kind of a book\'s rows (a "Shipping line" among the parties) narrows to that\n kind, or its picker offers every row.\n\nA field\'s own `required` is not here: the field carries it, and every write\npath refuses the row by field name from it. `unique` is the text field\'s own\nclause (\xA7 `text`) \u2014 an add says so at the control before the column does. A\nfield the workspace writes (a formula, a rollup, a lookup, an autonumber, a date\nwith `derive_from`) is never asked, written or edited.\n\n## App tree\n\nAn app is a tree of components, assembled the way a page is written in JSX: pick a component, bind it to a table by\nname, and it draws well with no props, because its defaults come from the table\'s facts (\xA7 Records, \xA7 Status,\n\xA7 Write rules, \xA7 Field). A prop overrides a default at its own node. No prop says how anything looks: the runtime\ndraws each concept one way, and a screen the job needs that no component states is told to the person \u2014 the job,\nwhat the app drew, the word wanted \u2014 never props bent to approximate it.\n\nEach component is a page of its own: `reference/app-node`, `reference/list-node`, `reference/record-node`, `reference/section-node`,\n`reference/side-node`, `reference/page-node`, `reference/outside-node`, `reference/tab-node`, `reference/field-node`, `reference/act-node`, `reference/check-node`,\n`reference/reading-node`, `reference/progress-node`, `reference/statement-node`, `reference/export-node`, `reference/book-node`,\n`reference/query-node` and `reference/workflow-node`. Beside them: `reference/changing-an-app` and `reference/what-an-app-may-write`, `reference/layouts`, `reference/time-and-place-layouts` and\n`reference/publish-desk` for a List\'s `layout`, `reference/composed-page`, `reference/tasks`, and the rules, `reference/app-rules`,\n`reference/reading-rules`, `reference/statement-rules`, `reference/check-rules`, `reference/list-rules`, `reference/record-rules`, `reference/act-rules` and `reference/template-rules`.\n\n<!-- generated:start app-components -->\n\n| Component | Draws | Holds |\n|---|---|---|\n| [App](#app-node) | the launcher tile, the app\'s words, its Lists, and its API | List, Tab, Reading, Query, Workflow |\n| [List](#list-node) | rows of an entity: the register, a record\'s child rows, a roster\'s cells, related rows, a log | Tab, Reading, Progress, Export, Act, Check, Book, Record, List |\n| [Record](#record-node) | what a row opens: the head and the parts | Field, Section, Side, Page, Outside, List, Reading, Progress, Statement, Act |\n| [Section](#section-node) | a titled part of a record, and optionally a step of its path | Field, List, Reading, Progress, Statement, Act |\n| [Side](#side-node) | the part the rest is worked from, beside them | Field, List, Reading, Progress, Statement, Act |\n| [Page](#page-node) | a part on its own page, off the record\'s scroll | Field, List, Reading, Progress, Statement, Act |\n| [Outside](#outside-node) | what one record shows, and which acts it allows, to a person with no seat who opens a link a member made of it | Field, List, Act |\n| [Tab](#tab-node) | one view of a List\'s rows over a period, or one sheet of a dashboard | Reading, List |\n| [Field](#field-node) | one field: a control, a gallery, prose, a link and its picker, the rows filed under a back-link | \u2014 |\n| [Act](#act-node) | a verb: what pressing it does | \u2014 |\n| [Check](#check-node) | a formula of the rows that warns, or refuses the writes and acts it names | \u2014 |\n| [Reading](#reading-node) | a number over rows: one figure, its parts, a grid, per period, steps; or a record\'s own figure | \u2014 |\n| [Progress](#progress-node) | the work against its plan, day by day | \u2014 |\n| [Statement](#statement-node) | a figure built from figures of the record | \u2014 |\n| [Export](#export-node) | a report file of the rows in view | \u2014 |\n| [Book](#book-node) | another table read as part of the register | Record, Act, Check |\n| [Query](#query-node) | nothing: a read for agents | \u2014 |\n| [Workflow](#workflow-node) | nothing: a verb for agents | \u2014 |\n\n<!-- generated:end app-components -->\n\n### A node\n\nA node is `{ "type": <Component>, "key"?, ...props, "children"?: [...] }`. `type` names the component, `key` its\nname among its siblings, the other keys its props, `children` the nodes it holds. An unknown prop is refused.\n\n- **Two shorthands, both for Field.** A bare string child is a Field (`"due_date"`); an object with `field` and no\n `type` is a Field with props (`{ "field": "price", "edit": false }`). Both work in every prop that lists fields:\n `create`, `row.subtitle`, `add`, and the `children` of a Record or a part. A column is a field\'s alias, or\n `{ "field", "edit": true }` for the line\'s own control, and takes no other prop.\n- **Fields and options are named by alias**, never by label. A tagged value is an object, never a magic string.\n- **`false` turns a default off at its node:** `"figure": false` drops the row\'s figure, `"edit": false` makes a\n field read-only, `"create": false` and `"remove": false` take a List\'s add and Delete away, `"readings": false` its\n band, `"row": { "status": { "edit": false } }` the status buttons (another app moves the stage) and\n `"row": { "image": false }` the picture.\n\n### Keys and addresses\n\nA key is unique among its siblings, the way a React key is. A node that binds data takes its key from what it binds:\n\n| Node | Key when none is stated |\n|---|---|\n| Field, Check | its field alias |\n| List, Book | its entity alias |\n| Record | `record` |\n\nEvery other node states its key. Three kinds of key are unique across the whole app, because each is an API name:\nan Act\'s (its workflow is `act_<key>`, or the one it names), a Reading\'s (its query name for `run_app_query`), and a Query\'s or\nWorkflow\'s.\n\nA node\'s **address** is the path of keys from the app, such as `customer/record/orders`; the app itself is `/` (the `in` of a node at the root), and its own key is part of no address. A patch may use any ending\nof that path that names exactly one node (`orders`). The exact path wins where one exists; an ending naming two\nnodes is refused, and the refusal lists both paths. `get_app` prints the tree with each node\'s address.\n\n### Values\n\n| Value | Written |\n|---|---|\n| now, the reader, an asked input | `{ "now": true }`, `{ "me": true }`, `{ "input": "name" }` |\n| a computed value | `{ "formula": "{cost} * 1.2" }` over the acted row\'s fields |\n| a call\'s own value, in a Query | `{ "param": "name" }` |\n| cleared | `null` in an Act\'s `set` |\n| an option | its alias |\n\nA **condition** (`when`) maps a select to a list of its option aliases and a yes/no to `true` or `false`; every entry\nholds. A number (a number, currency or percent field, or a formula or rollup yielding one) takes a comparison with any\nof `gt`, `gte`, `lt`, `lte` (at least one, never both `gt` and `gte` or both `lt` and `lte`), each a number; a date\ntakes the same keys with a day (`"2026-03-01"`), a moment on a datetime (`"2026-03-01T09:30"`) or `{ "now": true }`\n\u2014 today, or the current minute on a datetime, read when the rows are, so overdue is `{ "due_on": { "lt": { "now": true } } }`;\nand any field but a yes/no takes `{ "empty": true }` or `{ "empty": false }`. A comparison on a field it does not fit is\nrefused, naming the field, its type and what it takes. A `when` on an Act, a Section or other block, and a List\'s `where`\n(the register\'s or a child List\'s) take these; a child List that adds, picks or expects rows refuses a comparison in its\n`where`, since a row it holds could stand outside it. A **narrowing** (`opens`, a Reading\'s, a Tab\'s or an Export\'s `where`) takes options, yes/nos and also `{ "me": true }` on a member\nfield, the reader; `where` on the register, a log or a record\'s child rows fixes the rows on the server for every reader, so it refuses\n`{ "me": true }` \u2014 open on it with `opens`; on a record\'s related rows (`same`, `app`) and on a dashboard\'s List it\nnarrows by the reader and takes it, and those two take no comparison.\n\n## Changing an app\n\n1. `get_workspace` with `kind: "app"` names the app. `get_app` prints the `tree:`, the `node addresses:` (the address of\n each node the app draws, `(default)` on those the tree leaves to the platform) and the `version` line, whose id is the `base_version_id` to send. A member who is not the\n app\'s author is shown no tree. Keep the tree: no tool reads an earlier one.\n2. `set_app` with `app_id`, `base_version_id` (that version: a newer write is refused, not overwritten) and `nodes`,\n (a refused change writes nothing, so `dry_run` is for reading the permissions it would grant and remove). Every answer\n states the permissions the change grants and removes.\n3. `screenshot_app` shows the result.\n\n`nodes` maps an address to a patch of that node. Read against the customers List of the account review (\xA7 A complete\nworkspace, abridged), the call below makes `tier` read-only, moves the Orders Section ahead of Account, adds an Act to\nAccount and removes the Reading:\n\n```json\n{\n "type": "List",\n "entity": "customer",\n "children": [\n { "type": "Reading", "key": "ordered_by_tier", "by": "tier", "value": "total_ordered" },\n {\n "type": "Record",\n "children": [\n { "type": "Section", "key": "account", "title": "Account", "children": ["tier"] },\n { "type": "Section", "key": "orders", "title": "Orders", "children": [{ "type": "List", "entity": "order" }] }\n ]\n }\n ]\n}\n```\n\n```json\n{\n "app_id": "<id of the app>",\n "base_version_id": "<current_version_id>",\n "nodes": {\n "tier": { "edit": false },\n "orders": { "before": "account" },\n "mark_gold": { "type": "Act", "in": "account", "label": "Make gold", "when": { "tier": ["standard"] }, "set": { "tier": "gold" } },\n "ordered_by_tier": null\n }\n}\n```\n\nEach entry applies in order:\n\n- **An existing node:** a JSON merge patch (RFC 7396) of its props; `null` drops a prop, and a list (`children`,\n `columns`) replaces the node\'s whole.\n- **`null`:** removes the node and everything under it.\n- **An address naming no node, with `type` and `in`:** adds a node there; its key is the address\'s last word, and a\n Field states `field` in place of `type`.\n- **`in`, `before`, `after`:** move a node into another, or ahead of or behind a sibling.\n\nThe app\'s own props (`name`, `description`, `icon`, `color`, `words`, `scope`, `period`) change\nthrough `body`, a merge patch over the App node. `dry_run` checks a change and writes nothing.\n\n| To change | State |\n|---|---|\n| a field read-only | `"edit": false` on its Field, or on the Section, Side, Page or Record that draws it |\n| a column added or dropped | `columns` of the List, every column kept: a List that never stated `columns` draws none beyond the row\'s title and its `records` line |\n| a button | an Act node (\xA7 Act node); `confirm` makes its press ask first |\n| a label | the table\'s field `label` in `set_tables`, for every app; for this app alone, `body: { "words": { "<table>": { "fields": { "fee": "Consultation" }, "options": { "urgency": { "rush": "Same day" } } } } }`; a List\'s `label`, a Section\'s `title`, an Act\'s `label`, `add_label` on the add. A Field\'s `label` heads only a gallery or prose block |\n| the order of nodes and rows | `before` or `after` a sibling, or `in` another parent, in the node\'s entry (a Section ahead of another, a Field ahead of another in its Section); a List\'s `columns` read in the order stated; its rows read in its `sort`: `{ "sort": { "field": "collected_on", "desc": true } }` |\n| the row\'s second line | `row: { "subtitle": ["mechanic"] }` on the List: up to two fields under the title, in place of the table\'s `records` line, which stays as it is |\n| a picker\'s add | `add` on the link Field: `{ "add": ["species", "microchip"] }` asks those fields of the new row; `false` makes the picker only pick. It holds in every picker of that table in the app |\n| a linked row fixed or deleted where its link stands | `record` and `remove` on the link Field, or a row `title` that is a link: `{ "add": ["name", "kind"], "record": ["name", "kind"], "remove": true }` \u2014 the link\'s card opens the row in the drawer, saving those fields, with its Delete. Stated once per table, it holds on every card of that table in the app; a table a List of the app draws is worked in that List, and the statement there is noted and grants nothing |\n| a headline figure | a Reading in the List: `{ "type": "Reading", "in": "orders", "key": "prepaid_owed", "label": "Prepaid owed", "value": "prepaid", "where": { "stage": ["awaiting_payment"] } }` sums the rows in view; without a `value` it counts them |\n| a calendar and its date | `layout: "calendar"` on the List, and the date it places each row on is the first of `row.due`: `{ "layout": "calendar", "row": { "due": ["pickup_on"] } }`; absent, the table\'s `records.due` |\n| a record\'s child lines | a List inside the Record\'s Section over the child table: `{ "type": "List", "in": "details", "entity": "repair_line" }`; the child table (one link back to the record) and the record\'s rollup over its rows come from `set_tables` first. Lines priced off a book: `docs path="design/treatments/moves-and-numbers/priced-lines"` |\n\n**A node the tree leaves to the platform has an address too.** `get_app` lists the nodes the app draws, those the stored\ntree does not state marked `(default)`: the Record with its Sections and Fields, the status buttons, the band of readings.\nPatch one by its address and the call states it first: the nearest node the tree does state gains the default child that\nleads to it, as drawn (the whole Record under a List that states none, with the List\'s `row` picture and the `columns` the\ndefaults added; every Section of a Record that states no part; every status button of the app, each in the Section of its\nstage; every reading of the band, since stating one stands the others down), then the patch applies. Defaults\noutside that subtree stay defaults, and the answer\'s notes say what was stated. Removing every reading, or every status\nbutton, is refused: a set no node states is drawn again by the platform, and only `readings: false` or\n`row.status: { "edit": false }` on the List draws none. **A stated subtree stays as it is**: a\nfield or option the tables gain later is not added to it, so name it in the tree. `{ "due_date": { "edit": false } }`\nover a sparse tree makes that field read-only and takes away `saves <table>: due_date` alone. An address that ends several\ndrawn nodes is refused, as for stated ones.\n\n## What an app may write\n\nAn app states no list of what it writes: the tree derives it, and the generated workflows accept exactly that, so it is\nalso what an agent can do through the app\'s API. `get_app` prints it beside the tree.\n\n1. **A save of field `f` of table `E`** exists when a node draws `f` on a page of `E`\'s rows (a Field in a Record,\n Section, Side or Page, or a part of the head), the table lets a person write it (stored; not computed, stamped or the\n back side of a child\'s link), no Act of the app on `E`\'s rows writes it, and neither the node nor a container above it\n says `edit: false`. A row\'s stage is moved by an Act or the one button per move a status no Act moves gets, by hand only where `row.status` says `{ "edit": true }`, and by none where it says `{ "edit": false }`.\n2. **An add of `E`** exists for a List over `E` that writes its rows unless it says `create: false` (asking what it lists, and\n setting the link back to the record itself), and for an `add` on a Field drawing a picker of `E`. An absent `create`\n stands down where the table states no `singular` (nor the app\'s `words`), or where `where` keeps a select at an option an\n Act sets other than the one a new row starts at; a stated `create` list over such a table is refused. A roster\'s cells and\n a document are added only where `create: true`.\n3. **A Delete of `E`** exists for a List over `E` unless it says `remove: false`, and for the table of every Book of that\n List (a Book has no `remove` of its own). A roster\'s cells, a document and a desk\'s rows are deleted only where `remove: true`.\n `remove: false` takes this app\'s Delete away and no more: the record tools, the API and other apps still delete. A row no\n surface may delete is a table check with `on: ["delete"]`.\n4. **An act** exists for each Act node. Every field it sets, asks, files into, signs or records is the act\'s, and read-only\n everywhere else in the app.\n5. **The table\'s contract holds on every path** and is not restated here: required, unique, no overlap, field rules, table\n checks, stage owners, history and the read scope. (`natural_key` is read by an app\'s add and find-or-create alone.)\n\nDrawing is reading or saving; a List that writes its rows also adds and deletes them, until it says `create: false` or `remove: false`, and a List below a part or a Record that says `edit: false` writes, adds and deletes none.\n\n### What a table gives a component\n\n| Table fact | Feeds |\n|---|---|\n| The title | every row\'s name: Lists, a Record\'s head, link chips, pickers, an add |\n| The row\'s line (`records` subtitle, figure, image, mark, party, description) | the row\'s line and head, cards, a checklist\'s photo |\n| The stage (`records` status) | the stage chips, a List\'s `opens`, the badge, an Act\'s moves, stage owners gating acts, a Section\'s `at` |\n| Deadlines (`records` due, settled, frees) | countdowns and overdue marks, the default sort, attention order, spans on lanes, calendars and rosters |\n| Bounds (`records` limits, gates, tolerance) | the figure\'s meter, wherever it stands |\n| Tasks and cadence (`records` task, cadence) | the ring on task rows, a roster\'s owed days |\n| Applicability and starts (`records` applies, starts) | a field shown and asked only where it applies; an add\'s starting values |\n| A field\'s `required`, `default`, `default_from`, `natural_key`, `unique` | what an add asks, and what it starts at |\n| A field\'s `notation`, `format`, options, `options_where` | money, units, chips, and what a picker offers |\n| The table\'s `read_scope` | which rows each reader reads, in every List |\n\n### Where the rules stand\n\n\xA7 App rules and the rule pages after it are every rule the check holds over an app, by id, whichever component it concerns. A\n**language** is decided by the app\'s own words: a tree names no locale, so what an app draws itself (its buttons, empty\nstates, refusals) is in the language its labels are written in, and `words` count as labels. Mix two languages in one\napp and it answers in two.\n\n## App node\n\nAn app draws its launcher tile, its words, its Lists and its API. It binds the tables it names: `set_app` fills\n`tables` with the id of every table alias the tree uses, and `templates` with that of every document template alias.\nA relabel on either side is a rename and no tree points at another table by accident.\n\n<!-- generated:start node-app -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The app\'s alias, its name among apps; the App node is addressed `/`, and no address in it starts with this key |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A Lucide icon name in kebab-case the launcher tile draws |\n| `color` | [colour](#select) | no | The launcher tile\'s colour |\n| `words` | map of alias \u2192 [Table labels](#table-labels) | no | A table \u2192 the words this app draws for it |\n| `scope` | alias | no | A required one-row link of the first List\'s entity: the app works inside one row of the linked entity |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `uses` | list of alias (at least one) | no | Tables the app\'s Query and Workflow nodes read without drawing them, by alias: they join the tables the app reaches, so a workflow may name them |\n| `period` | `"month"` \\| `"quarter"` \\| `"year"` \\| [Dashboard period days](#dashboard-period-days) | no | The period a dashboard opens on |\n| `tables` | map of alias \u2192 text | no | The id of each table the tree names, by its alias: filled by set_app, never written |\n| `templates` | map of alias \u2192 text | no | The id of each document template the tree names, by its alias: filled by set_app, never written |\n| `children` | list of ([List](#list-node) \\| [Tab](#tab-node) \\| [Reading](#reading-node) \\| [Query](#query-node) \\| [Workflow](#workflow-node)) (at least one) | no | The first List at the root is the register, and a second is another app; a dashboard holds Tabs, Readings and Lists that open in another app. Queries and Workflows stand here too |\n\n#### Table labels\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | no | The table\'s name in the plural (a register\'s heading, a block\'s, a link\'s picker) |\n| `singular` | text | no | One row of the table, as the add and empty sentences name it |\n| `fields` | map of alias \u2192 text | no | A field alias \u2192 the words this app draws wherever it names the field; the table\'s field keeps its own label |\n| `options` | map of alias \u2192 map of alias \u2192 text | no | A select field alias \u2192 option alias \u2192 its words |\n\n#### Dashboard period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | `30` | yes | The last 30 days through today \u2014 the one span of days a dashboard opens on |\n\n<!-- generated:end node-app -->\n\n- **The first List is the register**: the working list of one table. Every further List at the root is a **log** beside\n it, at most 3, each stating its `label`: read as what was brought into or done to the register\'s rows (the files\n imported, the runs of a batch), opened as a register of its own inside the app, its reads, acts and workflows the\n app\'s. Only the organization\'s members are shown a log. A log is a table or cards with no `Tab`, `Book` or `compose`;\n its table is never the register\'s, a Book\'s or another log\'s; none beside `scope`. A list people work every day as\n a desk of their own is another app.\n- **A dashboard** is an App with no register: it holds `Tab`s, `Reading`s and read-only `List`s that open in\n another app (`app`). One `period` windows every reading placed in time: this month by default, or `quarter`, `year`\n or `{ "days": 30 }`. `words` and `scope` belong to a register.\n- **Query and Workflow** nodes stand at the root and are never drawn.\n- **Writes:** nothing itself.\n\n## List node\n\nA List draws rows of one table. What its rows are depends on where it stands:\n\n- at the App root: the register (the first) or a log (each further one);\n- under a Record, Section, Side or Page: the record\'s child rows, filed through the child\'s one link to the record\n (`via` where it has several);\n- under a roster List: the rows that fill the roster\'s cells;\n- with `same` or `app`, or on a dashboard: related rows, read only.\n\n<!-- generated:start node-list -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Absent, the entity alias |\n| `entity` | alias | yes | The table whose rows the List draws |\n| `via` | alias \\| text | no | The child\'s link to the record, where it has several. A List that only reads may state `<link>.<link>`: rows linked to the record only through a row of their own \u2014 their one-row link to that row, then that row\'s one-row link to the record |\n| `same` | alias | no | A one-row link of the record: lists the rows sharing that link\'s row, the record left out, read only |\n| `where` | [narrowing](#app-tree) | no | A fixed narrowing on the server, for every reader: a select holds one of these options, a yes/no this value, a number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), a field is `{ "empty": true \\| false }`, and `<link>.<field>` keeps the rows whose one-row link names a row holding a select or yes/no so (a path holds nothing on an add) \u2014 every entry ANDed. A List that only reads (`same`, `app`, a Record with `edit: false`) also bounds a date or number by a field of the record it stands beside, `{ "record": "<field>" }`, read when the rows are: a window of the record\'s own days (`{ "published_on": { "gte": { "record": "period_start" }, "lte": { "record": "period_end" } } }`); a bound the record holds blank keeps no row, and the same rows are handed to an Act\'s paper that names the List. A record bound narrows what is listed; it is never an access bound \u2014 the table\'s row rules are. An add holds a select\'s option and a yes/no\'s value; a bound or a blank holds nothing on an add |\n| `expect` | alias \\| [Expect](#expect) | no | One line per option of this select, or per row of this catalog link |\n| `under` | alias | no | A one-row link to the document that covers the line; lines stand grouped under their document |\n| `app` | text | no | The id of the register app its rows open in |\n| `row` | [List row](#list-row) | no | How this List reads its rows, where it differs from the table\'s `records` |\n| `label` | text | no | What the app\'s lists and the tab strip call the register; absent, the entity\'s label. Never the noun of its rows \u2014 the table\'s own label and singular (this app\'s `words`) are |\n| `columns` | list of (alias \\| [Edit column](#edit-column)) | no | Fields read after the row\'s title; `{ field, edit: true }` is the line\'s own control, at most 2. Absent: none, the row reads its title and its `records` line alone |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | Fields the reader narrows the rows by every day, at most 3; the stage chips always stand. A field is a select, member, link, date, yes/no or number range; `tiers` bands a number or date |\n| `search` | list of alias (at least one) | no | Fields the search box reads; absent, every word of the row |\n| `group` | alias | no | A field the rows stand grouped by; a gantt draws the groups as its lanes |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order of the rows; absent, the first `records.due` soonest, else newest |\n| `opens` | [narrowing](#app-tree) | no | The view the register opens on, a chip the reader may take away |\n| `period` | `"today"` \\| `"week"` \\| `"month"` \\| `"quarter"` \\| `"year"` \\| [Period days](#period-days) | no | The period the List opens on, with Tabs; absent, this month |\n| `attention` | `false` | no | false: rows keep the list\'s own order; absent, those needing attention open first |\n| `readings` | `false` | no | false: the register leads with no band of figures; absent, rows that move, fall due or carry money lead with one. Never with Readings stated |\n| `layout` | [layout](#layouts) | no | How the rows are drawn: a word, or an object stating one layout with its data; absent, a table |\n| `compare` | [Compare](#compare) | no | Reads each row\'s before against its after: two files fields as a pair of photos, or two values of one type as one column |\n| `current` | alias | no | A child List of revisions: the child\'s date or number field that orders them. The row holding the latest value reads as current and the others fold under "superseded \xB7 N", one press to open; the record shows the current file through a lookup of the child ordered by that field |\n| `create` | boolean \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The fields an add asks. Absent: where this List writes its rows, the table\'s required fields, its natural key and its title; `false`: rows are not added here. Where the table states no `singular` (nor the app\'s `words`), or `where` keeps a select at an option an Act sets other than the one a new row starts at, an absent `create` adds nothing; the table\'s `singular` or a stated list adds. A roster\'s cells and a document add only where `true` |\n| `add_label` | text | no | The add button\'s words; absent, the platform\'s word and the entity\'s singular |\n| `remove` | boolean | no | Rows offer a Delete. Absent: they do where this List writes its rows; `false`: this app\'s Delete is gone, while the record tools, the API and other apps still delete \u2014 a row no surface may delete is a table check with `on: ["delete"]`. A roster\'s cells, a document and a desk\'s rows delete only where `true` |\n| `pick` | `true` | no | Also picks rows of the child that no record holds yet |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `children` | list of ([Tab](#tab-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Export](#export-node) \\| [Act](#act-node) \\| [Check](#check-node) \\| [Book](#book-node) \\| [Record](#record-node) \\| [List](#list-node)) (at least one) | no | What the List holds: its Tabs, Readings, Progress, Exports, Acts and Checks, its Books, the Record a row opens, under a roster the List of its cells, and in a child List with `under` the List of the documents the lines stand under (the one place a document is added or deleted) |\n\n#### List row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | [List row title](#list-row-title) | no | The row\'s title field, stated for its props alone: `edit: false` reads it, `add` is its picker\'s add, `false` its picker only picks; where it is a link, `record` and `remove` how its card works the row it names |\n| `subtitle` | list of (alias \\| [Field](#field-node)) (1\u20132) | no | Up to two fields read under the title, in place of the table\'s |\n| `figure` | alias \\| `false` | no | The one number a row stands for, in place of the table\'s; false, none |\n| `image` | alias \\| `false` | no | A files field that pictures a row, in place of the table\'s; false, none |\n| `due` | list of alias (at least one) \\| `false` | no | Dates that count down, in place of the table\'s; false, none |\n| `status` | `false` \\| `{ "edit": true \\| false }` | no | false: the stage is not shown. `{ edit: true }`: the stage is also moved by hand. `{ edit: false }`: nobody moves it in this app, another app does. Absent, the stage\'s moves are buttons |\n\n#### List row title\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `edit` | `false` | no | false: the title is read; absent, it is written where the table lets a person write it |\n| `add` | `false` \\| `true` \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The new row this field\'s picker offers asks these fields; `true`: the target\'s required fields, its natural key and its title. Stated once per target table, and it holds in every picker of that table in the app. `false`: this link only picks, in every picker of it the app draws, not only where it is written |\n| `record` | list of alias (at least one) | no | The fields of the row this link names that this app saves from the link\'s card, which opens the row in the drawer: stored fields of the linked table a person writes. Stated once per linked table, and it holds on every card of that table in the app, where no List of the app draws its rows. Absent, the card reads the row |\n| `remove` | `true` | no | true: the row this link names is deleted from the link\'s card. Stated once per linked table, with its `record`, and it holds on every card of that table in the app. Absent, the card deletes nothing |\n\n#### Edit column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The column\'s field |\n| `edit` | `true` | yes | true: the column is the line\'s own control, written in place |\n\n#### Expect\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The child\'s title when the record holds one row per value of it: a single select (one line per option) or a one-row link to a catalog (one line per row) |\n| `of` | alias | no | With a select: the record\'s own multi-select holding the options this record expects a line for, in place of every option |\n| `only` | list of alias (at least one) | no | With a select: the options this List holds a line for; never with `of` |\n\n#### Compare\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `before` | alias | yes | A field of the List\'s rows holding what stood before: a files field (the photo of the place before the work), or a value \u2014 text, a number, a date or a single select (the price a row held before a sheet changed it) |\n| `after` | alias | yes | Another field of the same type holding what stands after: the photo of the same place once the work is done, or the value the row holds now |\n| `edit` | `true` | no | The after is written on the line |\n\n#### Tiered filter\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row |\n| `tiers` | list of number (at least one) | yes | The breakpoints between its bands, ascending, in the field\'s stored unit: under the first, from each to the next, from the last up \u2014 each band worded in the field\'s notation; a filter picks several at once |\n\n#### Sort key\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field rows are ordered by |\n| `desc` | `true` | no | Latest or largest first; absent, soonest or smallest first |\n\n<!-- generated:end node-list -->\n\nA List takes only the props its place lists:\n\n<!-- generated:start list-places -->\n\n| A List standing in | Takes |\n|---|---|\n| the register (the first List of the App) and a log (each further one) | `key`, `entity`, `row`, `label`, `columns`, `filters`, `search`, `group`, `sort`, `opens`, `period`, `attention`, `layout`, `create`, `add_label`, `remove`, `readings`, `where` |\n| a record\'s child rows, as a table, a checklist or a desk | `key`, `entity`, `via`, `label`, `columns`, `create`, `add_label`, `where`, `under`, `filters`, `search`, `group`, `sort`, `remove`, `pick`, `layout`, `compare`, `current`, `when`, `expect`, `row` |\n| a record\'s child rows with `layout` an agenda | `key`, `entity`, `via`, `label`, `columns`, `create`, `add_label`, `remove`, `layout`, `when`, `row` |\n| a record\'s child rows with `layout` `"timeline"` | `key`, `entity`, `via`, `label`, `create`, `add_label`, `remove`, `layout`, `when`, `row` |\n| a record\'s related rows, read only (`same`, `app`, or a Record that is `edit: false`) | `key`, `entity`, `via`, `label`, `where`, `columns`, `app`, `same`, `when`, `row` |\n| the documents a child List\'s lines stand under (`under`), a List inside it | `key`, `entity`, `create`, `remove` |\n| the cells of a roster, a List inside it | `key`, `entity`, `expect`, `create`, `remove`, `row` |\n| a dashboard\'s list, read only | `key`, `entity`, `label`, `where`, `columns`, `app` |\n\n<!-- generated:end list-places -->\n\n- **Children:** Tab, Reading, Progress, Export, Act, Check, Book, Record, and a List for a roster\'s cells. A Check or an\n Act stands in the List whose rows it works on: on each row (its \u22EF and the open row\'s), or with `on` on the ticked rows\n or every row in view.\n- **A child List\'s Record** holds `edit`, Fields and Lists only: its Fields are what the rows\' saves write, and a row\n opens in place under itself \u2014 its fields, then each List of its own children (an entity linking to the List\'s), stated\n as a child List one level up with its Acts, Checks and Record, at any depth. With `edit: false` the List only reads, as\n a related-rows List does: it states its `label`, takes none of `filters`, `search`, `group`, `sort`, `create`,\n `remove` or `layout`, and may reach rows linked to the record only through a row of their own (`via:\n "<link>.<link>"`).\n- **Documents:** in a child List with `under`, a List over the document\'s table (`key`, `entity`, `create`, `remove`,\n Acts, Checks and a Record of the document\'s Fields and Lists) is the one place the documents are added or deleted;\n `create` states `true`, and `remove` needs `create: true` beside it.\n- **Writes:** a List that writes its rows adds and deletes them \u2014 `create: false` and `remove: false` turn each off \u2014 `pick` links existing rows, an `edit: true` column writes that\n field on the line, and a layout move (a gantt bar, a lane\'s block, a plan\'s item) writes only fields the app already\n writes on the row\'s page.\n- **`create` asks:** `true` asks the title, the natural key and every required field a person writes \u2014 never a computed\n or stamped field, nor the link the place already sets. An `expect` list also asks the key it makes its lines by, and\n their picture. A list of fields overrides it; `false`, rows are not added here.\n- **`row`** overrides what the table\'s `records` say of the register\'s rows in this List alone (a cashier\'s figure is what\n is still owed): the subtitle, figure, image and deadlines stated replace the table\'s. A child List, a related List and\n a roster\'s cells read their rows as their table does, so their `row` states only a picker\'s add and a link\'s card (on its title or\n subtitle) or the stage moved by hand (`row.status`).\n- **`filters`** are the fields a reader narrows by every day, none to three \u2014 a quota never; each earns its place by\n daily use. A date filters by a range of days, a number by a range in its unit, `{ "field", "tiers" }` by the bands its\n breakpoints cut. A threshold the business names ("large orders") is a formula yes/no, never a range. The stage chips\n always stand.\n- **`opens`** is the view the register opens on, each entry a chip the reader may take away; absent, the open work\n (rows not at a `closed` stage).\n- **`expect`** makes one line per option of a select, or per row of a catalog link, and an empty line adds its row\n (a checklist of papers per case). **`under`** groups lines under the document that covers them.\n- **`compare`** reads each row\'s before against its after: two files fields as a pair of photos, or two values of one\n type as one column under the after\'s label.\n\n## Layouts\n\nA List\'s `layout` is how it draws its rows: a word (`"table"`, `"cards"`, `"calendar"`, `"roster"`, `"timeline"`) or an\nobject stating one layout and the data it reads (`{ "gantt": \u2026 }`, `{ "lanes": \u2026 }`, `{ "plan": \u2026 }`, `{ "agenda": \u2026 }`,\n`{ "checklist": \u2026 }`, `{ "desk": \u2026 }`). Absent, a table: the word `"table"` is the register\'s, and a child List states a\nchecklist, a desk, an agenda or `"timeline"`, else leaves `layout` out. Each layout reads what the rows already state \u2014 their\n`records` line, stage and dates \u2014 and the keys a layout adds stand on its section: here for an agenda and a checklist, \xA7 Time and place layouts for the rest, \xA7 Publish desk for a desk. Which one a job needs: the\n`design` reference. The register lays out as a table, cards, a calendar, a roster, a gantt, lanes or a plan; a record\'s\nchild rows as a table, a checklist, a desk, an agenda or a timeline.\n\n### Table\n\nAbsent `layout`, each row is one line: the table\'s `records` entry \u2014 its picture or mark, title and the line under it \u2014\nthen the stage, the `columns`, and the figure at the right; the title, the stage and a column of days or amounts sort\nthe rows by their header. A table groups its rows by `group`; a table or cards reads `Tab`s and `Book`s.\n\n### Cards\n\n`"cards"` draws each row as a card led by its picture \u2014 the `records` `image`, or the List\'s `row.image`: its title and\nthe line under it, its stage and what needs the reader (a deadline, a warning), then its figure and the `columns` at its\nfoot. Cards are for rows found by their picture (a product, a vehicle, a damage photo); rows with no picture read better\nas a table.\n\n### Calendar\n\n`"calendar"` places each row on its day \u2014 a month, a week, a day or a list (a phone reads the list or a day): at its\nhour where the date holds one, over its span where the row\'s line holds a second date after it, its line\'s other words\nand the `columns` after its title. The calendar reads the window in view, and its count and the stage chips count that\nwindow, opening on every row.\n\n### Roster\n\n`"roster"` draws the List\'s rows down the side and the days of a week or a month across; the roster\'s cells are a List\ninside it over the table whose rows fill the days \u2014 each stands on one row by its single link to it, and on the first\ndate of its line (a shift on a person\'s day, a run on a vehicle\'s), on every day through the second date where its line\nholds one. A cell reads its row\'s stage, else the first single select on its line (a shift\'s kind), several rows their\ncount. A day holding none stays empty: nothing was due, which is never an absence. A filled cell opens its row; an empty\none adds a row there, the register row and the day set. With `expect` on the cells (a single select of those rows) the\ncolumns are its options in place of days \u2014 a checklist of papers \u2014 and `of` names the ones each row expects: a gap among\nthem reads as one and adds its row, the others stand blank, and each row reads what it holds of them, `2/3`. Over days, a\nroster reads the `cadence` its table\'s `records` states (a site\'s diary page, Monday to Saturday from breaking ground to\nhandover): a past day owed with none is marked missing and counted, and each row reads the days it holds of those owed. A\nroster states no `columns` and needs no readings: its legend counts each state in view.\n\n```json\n{ "type": "List", "entity": "operator", "layout": "roster", "filters": ["depot"], "children": [{ "type": "List", "entity": "run" }] }\n```\n\n### Timeline\n\n`"timeline"` reads a record\'s child rows as a log of dated entries, newest first, with a composer where the app adds\nentries, each corrected and removed where it stands. It is never the record\'s status history: the rail shows each\nmove. A timeline\'s moment is its child\'s first date, and who its member field. Rows each what happened on a day and who \u2014\na date on their line, a member, no stage and no figure \u2014 read best as a timeline.\n\n### Agenda\n\n`{ "agenda": { \u2026 } }` reads a record\'s child rows as planned entries on a time axis: by day, soonest first, each with its\npicture and the day that is today marked. The day is the first date on the child\'s row line (its `records` title or\nsubtitle); within a day entries follow a datetime there, else the first single select after the date. An entry with no day\nyet is a plan, listed first and dated in place. `start` is the record\'s date that is the first day: each day\'s heading\ncounts from it (Day 1, Day 2).\n\n<!-- generated:start layout-agenda -->\n\n#### Agenda keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The record\'s date that is the first day: each day\'s heading counts from it; absent, the weekday and the date alone |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.agenda` | An agenda List\'s child names a date on its row line (`records` title or subtitle); the layout\'s `start` is a date of the record. |\n| `list.agenda-note` | *Noted, never refused:* Rows each on a day and within it (at an hour, in a part of the day) listed as a table: a List with `layout: { agenda }` draws them by day. |\n| `list.timeline` | A timeline List\'s child holds a date to place its entries by. |\n| `list.timeline-note` | *Noted, never refused:* Rows each what happened on a day and who \u2014 a date on their line, a member, no status and no figure \u2014 listed as a table: a List with `layout: "timeline"` draws them as a log, newest first. |\n\n<!-- generated:end layout-agenda -->\n\n### Checklist\n\n`{ "checklist": { \u2026 } }` reads a record\'s child rows, which are tasks, as a checklist of checks: each one\'s mark \u2014 passed\nat a `closed` option, failed at a `failed` one, else still to check \u2014 its title, its columns as its words (a text column\nits note, on its own line), and on a line under its title its photo and its clause. Never grouped nor written in place.\n`passed` alone sets the mark, so rows with no `task` and no status are checks: a measured yes/no is the verdict. With\n`expect` on the standard\'s items, each item is a line marked the same way (a missing one still to check), filled in\nplace by the block\'s `columns` (a measured value, a note, a photo), so `photo` and `clause` are not named.\n\n<!-- generated:start layout-checklist -->\n\n#### Checklist keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photo` | alias | no | The child\'s files field holding the photo each row is checked by; absent, the child\'s `records.image` |\n| `clause` | alias | no | The child\'s one-row link to what each row is checked against \u2014 a standard\'s clause, a rule |\n| `failed` | list of alias (at least one) | no | Options of the child\'s status a row stands failed at \u2014 none of them closed |\n| `passed` | alias | no | The child\'s yes/no field (stored or a formula) setting each row\'s mark: yes passed, no failed, empty still to check |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.checklist` | A child List with `layout: { checklist }` reads a task entity\'s rows as checks \u2014 never under documents, grouped, edited in place or picked together by an Act over several, its columns no files; `photo` names a files field of the child, `clause` a one-row link of it, `failed` options of its status, none `closed`; `passed` a yes/no of it, stored or a formula, setting each mark (yes passed, no failed, empty still to check) and standing without `task` or a status. With `expect` the lines of a standard are the checks, each marked and filled by the block\'s columns (a measured value, a note, a photo), so `photo` and `clause` are not named. |\n\n<!-- generated:end layout-checklist -->\n\n## Time and place layouts\n\nThe layouts that place a List\'s rows in time or on a plan. Each is a `layout` object stating the data it reads.\n\n### Lanes\n\n`{ "lanes": { "by": \u2026 } }` draws each row of the linked table as a lane (a chair, a machine, a room), empty ones too,\nand each row of the List as a block from the first date on its line to the second: over the hours of one day where the\nfirst holds its hour, else over the days of one day, a week or a month. A block reads the row\'s name and the first of its\n`columns`; a free stretch between blocks adds a row there, its lane and its start set, and its end where the next block\nbounds it. Rows naming no lane stand first, in a lane of their own; where the app writes the link, each block moves to\nanother lane from its menu. Inside a `scope`, the lanes are the ones whose own one-link names that scope\'s row. A span\'s\nend holds its day unless `records` names it the day the row `frees`.\n\n`loads` names what a lane carries: `{ "weight": "payload", "volume": "box" }` sums each block\'s number and reads it\nagainst the lane row\'s own, at most two. A lane holds its blocks while they stand, so it reads the most they add up to at\none time in the window, named, past its bound, by how much and on which day or hour. The two are in one unit, or two of\none dimension. A bound a single row must keep (no parcel heavier than its van) is a `max` on the field through the link,\nwhich refuses the move.\n\n<!-- generated:start layout-lanes -->\n\n#### Lanes keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `by` | alias | yes | A one-link of this entity whose target\'s rows are the lanes (a chair, a machine, a room), each drawn even when empty |\n| `loads` | map of alias \u2192 alias | no | What a lane carries against what it holds, at most 2: a number of this entity each block adds \u2192 a number of the lane\'s row it is held within, in one unit or two of one dimension |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.lanes` | `layout: { lanes }`: `by` is a one-row link to an entity with a `records` entry, the rows name a date on their line (a field of the row itself, or a lookup of its parent\'s date), a lane block\'s two ends are read alike (two days or two moments), and `loads` stands with it. |\n| `list.loads` | A lanes layout\'s `loads` names one to 2 numbers a lane block adds \u2014 never a percent \u2014 each against a number of the lane\'s row, in one unit or two of one dimension. |\n\n<!-- generated:end layout-lanes -->\n\n### Gantt\n\n`{ "gantt": { \u2026 } }` draws each row as one bar on one axis of time: `{ "start": "etd", "end": "eta", "milestones":\n["cut_off"], "planned": { "end": "booked_eta" }, "progress": "done", "after": "waits_on" }`. `end` defaults to the table\'s\nfirst `due`, and a table with neither is refused; `start` and `end` are two dates of the row, both days or both moments,\nand each planned date is read as `start` is. `progress` is a percent (0\u2013100); absent, a row whose record lists its tasks\nfills to how many are done. `after` is a link to rows of the same table, each ending before the row starts. The bars\nstand in the lanes of the List\'s `group`. Where the app writes the row and a person writes a bar\'s date, the bar is\ndragged through the row\'s save; a computed date, one an Act sets, or a row the app does not write drags nothing.\n\n<!-- generated:start layout-gantt -->\n\n#### Gantt bar\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of this entity each row\'s bar starts at (a sailing, a hire\'s start, a task\'s start) |\n| `end` | alias | no | The date each row\'s bar ends at, read as `start` is \u2014 two days or two moments; absent, the entity\'s first `due` |\n| `milestones` | list of alias (at least one) | no | Dates of the row marked on its bar\'s line as diamonds, each named by its label (a cut-off, a delivery, the end of free time) \u2014 never `start` or `end` |\n| `planned` | [Gantt plan](#gantt-plan) | no | The plan each row is read against, drawn as a thin bar under its own \u2014 each date read as `start` is; a missing one is the bar\'s own |\n| `progress` | alias | no | A percent of this entity (0\u2013100) filling each row\'s bar as far as the work is done; absent, how many of the row\'s tasks are done fills it where its record lists them, else the bar wears its status\'s tone |\n| `after` | alias | no | A link of this entity to its own rows each row waits on \u2014 it starts once they end (finish-to-start), an arrow from each; a row starting before one ends is drawn in the danger tone |\n\n#### Gantt plan\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The date the row was planned to start at |\n| `end` | alias | no | The date the row was planned to end at (a booked arrival) |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.gantt` | `layout: { gantt }`: `start` and `end` (absent, the entity\'s first `due`) are two different dates of the row, both days or both moments; each diamond (`milestones`) another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity. |\n\n<!-- generated:end layout-gantt -->\n\n### Plan\n\n`{ "plan": { \u2026 } }` draws things that stand at a place \u2014 pallets in a warehouse \u2014 on a plan of the places. The places are\nthe rows of a table whose `records` states a `plan_place` (a bay, an open area or a landmark, each at its spot on a sheet\nof the plan); `at` names the one-link of the List\'s rows to a place and, for an item in a bay, the row (`across`) and the\ntier (`up`) it stands in, counted from 1. A bay is drawn as its rows and tiers, an area with a count, a landmark as a\nlabelled frame; the reader sees a plan, one bay enlarged, and the same plan in 3D. The List may read other tables as\n`Book`s: one plan of every table\'s items. Each item carries `__reach` \u2014 how many items stand in the way of taking it out\n(those above it where the place is `stacked`, and those in the rows between it and the side `reach_from` says the bay is\ntaken from) \u2014 so the list, its export and an agent\'s read all carry it; `<table>_plan_load` counts the rows each place\nholds. A move through the app is refused unless it clears the place, row and tier all three, or places the item at\na free cell of a bay \u2014 resting on a present item where the place is `stacked` \u2014 or at an area. Every other write (a grid edit, an import, a tool) holds one item to a cell only where the item\'s table states `unique: [<at>, <across>, <up>]`; a plan over a table stating none is noted. A plan opens on every row\nits `where` keeps, with no stage chip narrowing it. Rows with no place wait in a tray beside the plan. An Act that asks exactly a place (a link to the plan\'s places) and two numbers (the row and the tier) is answered by a pick on the plan, never by a form. On a desk a press opens a row\'s record in the panel beside the plan.\n\n```jsonc\n// table "bay"\n"title": "code",\n"records": { "plan_place": { "kind": "kind", "x": "x", "y": "y", "across": "rows", "up": "tiers", "reach_from": "taken_from", "stacked": "is_stack" } }\n```\n\n```json\n{ "type": "List", "entity": "pallet", "filters": ["line"], "layout": { "plan": { "at": "bay", "across": "row", "up": "tier" } } }\n```\n\n```jsonc\n// table "pallet": one item to a cell on every write, not only through the app\n"unique": [["bay", "row", "tier"]]\n```\n\n<!-- generated:start layout-plan -->\n\n#### Plan keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `at` | alias | yes | A one-link of this entity to an entity whose `records` states a `plan_place`: where each row stands |\n| `across` | alias | yes | A number field: the row of a bay a row stands in, counted from 1; empty at an area |\n| `up` | alias | yes | A number field: the tier of a bay a row stands at, counted from 1 at the ground; empty at an area |\n| `fill` | alias | no | A single select the items are coloured by; absent, the first single select among the register\'s filters |\n| `mark` | [Plan mark](#plan-mark) | no | A row holding the option says the option\'s label on its box and is hatched in the option\'s own colour, and the legend names it by the label; in books, the select is matched by option alias, and a book lacking the select never marks its rows |\n| `size` | [Plan size](#plan-size) | no | Each row\'s box as it stands in the world, in the one length unit its places\' `size` states: the option the row holds of the select names its box\'s width across, depth along and height; in books, matched by option alias, as `mark` is; a row holding an option it does not name is drawn at its place\'s cell |\n\n#### Plan mark\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A single select of this entity |\n| `option` | alias | yes | An option alias of that select |\n\n#### Plan size\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A single select of this entity |\n| `options` | map of alias \u2192 [Plan size box](#plan-size-box) | yes | The box of each option alias of that select |\n\n#### Plan size box\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `across` | number | yes | The box\'s width across the bay\'s rows |\n| `along` | number | yes | The box\'s depth along them |\n| `up` | number | yes | The box\'s height |\n\n#### Plan place\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `kind` | alias | yes | A single select, options aliased exactly `bay`, `area`, `landmark`: a bay holds rows and tiers, an area neither, a landmark nothing |\n| `sheet` | alias | no | The plan sheet it is on: a single select, or a one-row link to an entity whose `records` states its `image` \u2014 the sheet\'s drawing, which the places are drawn over (with `scale`) |\n| `scale` | alias | no | A number field of the sheet entity `sheet` links to: how many cells wide its drawing is, the plan\'s scale. Required where that entity states an `image`, and only then |\n| `x` | alias | yes | A number field: left edge, in cells |\n| `y` | alias | yes | A number field: top edge, in cells |\n| `width` | alias | no | A number field: an area\'s frame width, in cells |\n| `depth` | alias | no | A number field: an area\'s frame depth, in cells |\n| `across` | alias | yes | A number field: a bay\'s rows |\n| `up` | alias | yes | A number field: a bay\'s most tiers |\n| `reach_from` | alias | no | A single select, options aliased exactly `first`, `last`, `both`: the side boxes leave a bay from; a bay that names none has no reach and no arrow |\n| `stacked` | alias | no | A boolean field: true where items at this place rest on the one below (a stack), so a tier above the first needs a present item under it, shows floating without one, and an item\'s reach counts the items above it; false or empty, items sit at any tier and only the rows between a bay\'s reach side and the item are in the way |\n| `size` | [Plan place size](#plan-place-size) | no | The real size of ONE cell of the place, for 3D: three number fields stating one length `unit` alike. Absent, a cell is drawn at the size most of the sheet\'s places state, a unit cube where none states one |\n\n#### Plan place size\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `across` | alias | yes | A number field: the width of one cell across the rows |\n| `along` | alias | yes | A number field: the depth of one cell along the place |\n| `up` | alias | yes | A number field: the height of one cell, one level |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.plan` | `layout: { plan }`: `at` is a one-row link to an entity whose `records` states a `plan_place`, `across` and `up` two different numbers of the row, `fill` a single select; a plan List has no Tabs, `group` or `attention`. A write that sets `at`, `across` or `up` is refused unless it clears all three, or places the row at a free cell of a bay (a row from 1 to the bay\'s `across`, a tier from 1 to its `up`, resting on a present item), or at an area with neither. |\n| `list.plan-unique` | *Noted, never refused:* A plan\'s items (the List\'s table and each Book) state `unique` on their place link, `across` and `up`: the app\'s moves refuse a taken cell, and the unique makes every other write hold it \u2014 the note names a table that states none. |\n| `list.plan-mark` | A plan\'s `mark` names a single select of the row and an option of it by alias: a row holding that option says its label on its box and is hatched in the option\'s colour, which the legend names by its label. A Book that holds the select must hold it as a single select with that option; a Book lacking it never marks its rows (`app.book-lacks`). |\n| `list.plan-size` | A plan\'s `size` names a single select of the row and, by option alias, the box (`across`, `along`, `up` \u2014 positive, in the one length unit the places\' `size` states) of each option: a row holding one is drawn as that box, any other at its place\'s cell. A Book that holds the select must hold it as a single select; a Book lacking it draws its rows at their places\' cells (`app.book-lacks`). |\n| `list.plan-box-size` | *Noted, never refused:* A plan whose places state a `size` and whose rows\' select options are ISO 6346 container size-type codes (`20DC`, `40HC`) states the box of each in `size`, from the ISO 668 dimensions, else every box is drawn at its place\'s cell. |\n\n<!-- generated:end layout-plan -->\n\n## Publish desk\n\nRows that are each a place the record is sent \u2014 a notice\'s boards, a listing\'s portals \u2014 are a record\'s child List with\n`expect` on the child\'s one-row link to the catalog of those places and `layout: { "desk": \u2026 }`:\n\n```json\n{\n "type": "List",\n "entity": "posting",\n "expect": "board",\n "columns": ["posted_link", "refusal"],\n "layout": { "desk": { "bodies": ["wording", "wording_abroad"], "text": "goes_out_as", "media": "pictures", "link": "page", "group": "reach", "network": "kind", "networks": { "company_page": "linkedin" } } }\n}\n```\n\nThe desk is where the post is written. `bodies` are the record\'s plain text fields it is written in (one per language,\nsay), each its editor between the list and the preview, saved as any field of the record is and drawn nowhere else on it.\nThe preview is `text` read over what is on screen: where `text` is a formula of the child, the desk computes that same\nformula over what is typed, so the post moves as it is written, and a place not ticked previews the words its row would\nhold. For that the formula reads only what a screen holds whole: stored values of its row, the record through lookups over\nthe link back to it, the place through lookups over `expect`, and formulas of the row over those \u2014 a rollup, a lookup\nthrough another link or the clock is refused, naming the field.\n\nEvery place the link admits is a line with a tick and that place\'s face: its picture, else the mark of the network it is on\nwhere `networks` names one. `group`, another single select of the place, stands the lines under its options in their\norder: a kind holding a ticked line is open, one holding none folded to its heading and its count. A tick makes the line\'s\nrow from its key alone \u2014 so nothing else the child requires is left without a default \u2014 and an untick deletes it until the\nrow\'s stage is `closed`: what went out is not taken back. It asks first only where a person wrote on the row. One line is\nworked beside the list and read as the post that place\'s feed shows: `text` is the child\'s words for that place, `media`\nand `link` the record\'s, and `network` the place\'s single select whose options `networks` names to a feed; an option not\nnamed reads on a plain card. Under the bodies stand the List\'s `columns` a person writes of the line\'s row; under the post,\nthe press copying its words beside the Acts of the child, then what the line and the `columns` hold that nobody writes\nthere (the link it went out at, why it was refused). A value an Act or a workflow fills is stated as that Act\'s, so the\ndesk never offers it as an empty box. Sending is an ordinary Act of the record.\n\n<!-- generated:start layout-desk -->\n\n#### Desk keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `bodies` | list of alias (1\u20133) | no | The record\'s plain text fields the post is written in, in order \u2014 one per language, say \u2014 at most 3: each its editor on the desk, between the list and the preview, and drawn nowhere else on the record |\n| `text` | alias | yes | A text of the block\'s rows \u2014 stored, or a formula yielding text \u2014 holding the words that go out on that row\'s destination. A formula is computed on the desk too, over what is typed: it reads stored values of its row, the record through lookups over the link back to it, the destination through lookups over `expect`, and formulas of the row over those \u2014 never a rollup, another link or the clock |\n| `media` | alias | yes | A files field of the record: the pictures or the video the post carries |\n| `link` | alias | no | A text field of the record holding a link (`format: "link"`), carried as the post\'s card |\n| `network` | alias | yes | A single select of the entity `expect` links to, saying which network each destination is on |\n| `group` | alias | no | Another single select of the entity `expect` links to \u2014 the destination\'s kind \u2014 the lines stand grouped by: each option a heading in the select\'s order, a group holding no ticked line folded to its heading and its count, destinations holding no option last |\n| `networks` | map of alias \u2192 `"facebook"` \\| `"instagram"` \\| `"threads"` \\| `"x"` \\| `"linkedin"` | yes | That select\'s options, each by its alias, to the network whose feed the post is previewed in \u2014 facebook, instagram, threads, x, linkedin. An option not named has no feed to imitate: its words read as a plain card |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.post` | A child List with `layout: { desk }` needs `expect` on a one-row link \u2014 never a select, nor with `under`, `pick`, a checklist layout, `compare` or a `create` list, its line made by a tick from its key alone: its `bodies` are plain text fields of the record a person writes, drawn nowhere else on it; its `text` is a text of the child, stored or a formula yielding text \u2014 a formula reading only stored values of its row, lookups through the link back to the record or through `expect`, and formulas of the row over those, never the clock; its `media` a files field of the record and its `link` a text field of the record; its `network` a single select of the entity the link names, and `networks` holds options of that select, by alias, each to one of facebook, instagram, threads, x, linkedin; its `group` is another single select of that entity. |\n\n<!-- generated:end layout-desk -->\n\n## Record node\n\nA Record draws what a row opens: the head, then the parts.\n\n- **The head:** title, subtitle, picture, mark, party, stage badge, figure and meter, deadlines, description \u2014 always the\n table\'s `records` facts, overridden by the List\'s `row`. A field the head draws is never also a Field of the record.\n- **No Record node:** under the register, the platform designs the record\'s Sections from the rows: the fields the\n head does not draw, child Lists, papers and Acts. A stated Record stands exactly as written, so it replaces them.\n Under a child List, an absent Record is every field of the child except the link back.\n- **Children:** either parts \u2014 `Section`, `Side`, `Page` \u2014 or, for a one-part record, `Field`, `List`, `Reading`,\n `Progress`, `Statement` and `Act` directly. The two are never mixed. Below the register \u2014 a child List\'s, a document\'s\n or a roster cell\'s \u2014 a Record holds `edit`, the Fields its saves write and the Lists of its rows\' own children; a\n Book\'s holds Fields and `edit` only.\n\n<!-- generated:start node-record -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `door` | `"page"` \\| `"drawer"` \\| `"beside"` | no | Absent, a page where a part is a step, a Side or a Page or there are more than 3 parts, else a drawer |\n| `comments` | `true` | no | The record carries comments; absent, nobody comments |\n| `edit` | `false` | no | false: nothing the record draws is written |\n| `compose` | [piece](#composed-page) | no | The page stated as a tree of finished pieces; a Record with `compose` holds no children |\n| `children` | list of (alias \\| [Field](#field-node) \\| [Section](#section-node) \\| [Side](#side-node) \\| [Page](#page-node) \\| [Outside](#outside-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | Parts (Section, Side, Page), or for a one-part record its Fields, Lists, Readings, Progress, Statements and Acts directly \u2014 never both; Outside parts stand beside either. Absent, the platform designs the Sections from the rows: the fields the head does not draw, child Lists, papers and Acts. Stated, it stands exactly as written |\n\n<!-- generated:end node-record -->\n\nThe door is a page where a part is a step, a Side or a Page, or where there are more than 3 parts; else a drawer. `door`\nis a judged choice of how a job is worked (one after another over a long record), so it is kept. From three parts on, the\nrecord is navigated by their titles: a page by a rail that jumps to each (a strip of the same names on a phone), a drawer\nby the same strip pinned over its scroll; with fewer, the parts just stack.\n\n`comments: true` lets people comment, and is opt-in: state it only where the job\'s people discuss a record. A door in the\nrecord\'s head then opens the comments in a panel beside it, with one composer. A record shows no activity and no audit\nlog: a history an app shows is table-backed \u2014 `records.status.dates`, `records.status.history`, an event table an act\nmakes \u2014 stated where the app shows it.\n\nA field, a Reading and an Act is each placed once on a record: the head, or one part. A Section or a List with `when` is\ndrawn only while each named select of the record holds one of its options.\n\nA page the parts cannot say is a **composed page** (\xA7 Composed page): `compose` states the record as a tree of finished\npieces, and the Record holds no children.\n\n## Section node\n\nA titled part of a record, and optionally a step of its path. A Section draws its Fields, Lists, Readings, Progress,\nStatements and Acts in reading order; its Acts stand at its foot.\n\n<!-- generated:start node-section -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique among its siblings; part of the node\'s address. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The part\'s heading |\n| `description` | text | no | A line under the heading |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `edit` | `false` | no | false: nothing in this part is written |\n| `at` | list of alias (at least one) | no | Options of the record\'s status during which this part is the work now: a step of its path |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | What the part draws, in reading order; its Acts stand at its foot |\n\n<!-- generated:end node-section -->\n\n- **`at`** names the stage options during which the section is the work now \u2014 only on a record that\n truly moves through those stages: the rail marks it done, now or waiting, and the section stands even holding nothing\n yet. It never hides a field, locks it or gates an act.\n- **Acts:** every Act on a record stands in the Section whose work it is. An Act moving the stage and offered at ONE step\n stands at that step\'s foot when it moves the work forward, to an exit later in the stage\'s options, or back to a step the\n work passes anyway; one offered at several steps, or moving back into a detour, stands in the Section it enters. A\n correction (`danger`, and not one of its step\'s outcomes) waits in the Section heading\'s \u22EF.\n- **A detour** \u2014 a stage the work only steps aside into from one step and leaves only back to it (a pause, a repair) \u2014\n stands before that step, its option and its Section alike.\n- **An exit** stands only once the record is in it, its reason there and its rail item apart from the path; until then the\n Act waits in the header\'s \u22EF.\n- **Writes:** its Fields and its Acts.\n\n## Side node\n\nThe one part drawn beside the rest \u2014 the photos looked at, the pool picked from. At most one Side to a record, on a page\ndoor, and never also a Page.\n\n<!-- generated:start node-side -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique among its siblings; part of the node\'s address. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The part\'s heading |\n| `description` | text | no | A line under the heading |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `edit` | `false` | no | false: nothing in this part is written |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | What the part draws, in reading order; its Acts stand at its foot |\n\n<!-- generated:end node-side -->\n\nIt holds what a Section holds. Writes: its Fields and its Acts.\n\n## Page node\n\nA part on its own page, off the record\'s scroll, with a way back \u2014 rarely, for a long related list or a workspace of its\nown that would make the record worse stacked. Never on a drawer, and never a step: its title names an address no other\npage shares (a letter or a digit at least), and not every part is a Page.\n\n<!-- generated:start node-page -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique among its siblings; part of the node\'s address. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The part\'s heading |\n| `description` | text | no | A line under the heading |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n| `edit` | `false` | no | false: nothing in this part is written |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Reading](#reading-node) \\| [Progress](#progress-node) \\| [Statement](#statement-node) \\| [Act](#act-node)) (at least one) | no | What the part draws, in reading order; its Acts stand at its foot |\n\n<!-- generated:end node-page -->\n\nIt holds what a Section holds. Writes: its Fields and its Acts.\n\n## Outside node\n\nWhat one record shows, and which acts it allows, to a person with no seat who opens a link a member made of it. A member\npresses Create link on a record while its status stands in `at`; the link opens that one record until it expires, is\nrevoked, or its act moves the status out of `at`. The person sees the stated Fields and Lists and presses the stated Acts\nand nothing else. It stands directly in a Record of a table with a stored status select: the register\'s, or the Record of\na List below the register, whose rows the link then opens.\n\n<!-- generated:start node-outside -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | no | Unique in the app: a link to the record names it. Absent, the title\'s words as a slug, written once and kept |\n| `title` | text | yes | The heading of the page the person without a seat opens |\n| `description` | text | no | A line under the heading |\n| `at` | list of alias (at least one) | yes | Options of the record\'s status during which a link to it opens; none is owned by a stage, and each Act moves the status out of them |\n| `children` | list of (alias \\| [Field](#field-node) \\| [List](#list-node) \\| [Act](#act-node)) (at least one) | yes | Exactly what the link shows and allows: Fields read as values, Lists of child rows read as they are, and the Acts the person may press, each moving the status out of `at` |\n\n<!-- generated:end node-outside -->\n\n- **Fields** are read as values, in order; a lookup, a rollup or a formula is computed for the link only if stated here.\n A Field states nothing but its alias.\n- **Lists** read the child rows under the record as they are: `columns`, `label` and `where`.\n- **Acts** are the link\'s alone: no member, visitor or agent presses them. Each states `key`, `label`, `confirm`, `when`,\n `requires`, `asks`, `set`, `template` with `into`, or `sign`. `when` absent is the status held in `at`; `set` moves the\n status to an option outside `at`, so each press ends the visit. `asks` are text, numbers, dates or listed options. A\n `sign` is by a text field the signer types, with no witness: the link is the attestation.\n- `at` names options no stage owns, and no two Outsides of a table open during the same option.\n- **Writes:** its Acts, and nothing else.\n\n## Tab node\n\nA Tab is one view of a List\'s rows over a period, or one sheet of a dashboard.\n\n- **In a List:** a register reporting a period \u2014 what stands at its end beside what came and went during it (the stock,\n the hires out, the tickets open) \u2014 reads its rows as Tabs over one `period` the reader picks above them. A Tab holds\n the rows `dated` within the period, those `open` at its end, or lists every row and draws what a child\'s rows came to\n in the period (`totals`). A Tab narrows its rows and holds no nodes; Readings stand beside it in the List and read the\n open Tab, or the one their `tab` names. Never date chips set by hand, and never a Tab per stage, which the chips split.\n- **On a dashboard:** a Tab holds its Readings and Lists as children. A dashboard with Tabs (2 to 5) draws a strip under\n its title that picks one, the first open.\n\n<!-- generated:start node-tab -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | Unique among its siblings; part of the node\'s address |\n| `label` | text | yes | What the tab holds, in the reader\'s words |\n| `dated` | alias | no | A date: the tab holds the rows dated within the List\'s period |\n| `open` | [Tab open](#tab-open) | no | The tab holds the rows open at the period\'s end: begun, and not yet ended |\n| `totals` | [Tab totals](#tab-totals) | no | Each row\'s figures over a child\'s rows in the period |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order this tab\'s rows read in; absent, the List\'s |\n| `children` | list of ([Reading](#reading-node) \\| [List](#list-node)) (at least one) | no | On a dashboard: the Readings and Lists this sheet draws. A Tab of a List holds none: Readings stand beside it in the List |\n\n#### Tab open\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The date a row begins on |\n| `to` | alias | yes | The date a row ends on, empty while it is open |\n\n#### Tab totals\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | An entity of this app whose rows each belong to one row of the register\'s entity: the rows the columns add |\n| `on` | alias | yes | A date of the child placing each row in time: stored, a formula yielding a date, or a lookup of a date through a one-row link \u2014 never a moment or a span of days |\n| `columns` | list of [Totals column](#totals-column) (1\u20136) | yes | The figures drawn in place of the register\'s columns after each row\'s name, each under its own alias and label |\n\n#### Totals column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The column\'s name: its address in the totals read |\n| `label` | text | yes | What the column holds, in the reader\'s words |\n| `value` | alias | no | A number of the child (stored, a formula or a rollup) summed over the rows; absent, the rows are counted. A figure read in each row\'s own unit or currency stands only where the child\'s select is a lookup, through `via`, of a single select of the register\'s entity |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the child\'s rows whose select holds one of these options, or whose yes/no is this value \u2014 every entry ANDed |\n| `window` | `"before"` \\| `"in"` \\| `"through"` | yes | Which of the child\'s rows, by `on`, the column adds: `before` those dated before the period\'s first day, `in` those from its first day through its last, `through` those on or before its last day |\n\n#### Period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | integer | yes | How many days through today, at most 366 |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.tabs` | A List\'s Tabs each have a `key` of their own, none the entity\'s alias or one a report template holds (`title`, `lines`, `readings`, `at`, `dates`, `period`, `per`); a Tab states one of `dated` and `open` (dates of its rows, `open`\'s two different) or `totals` (a child\'s rows windowed by the period); its `where` and `sort` hold as the List\'s do, and never narrow by the status, which its chips split. Only a table or cards List has Tabs, and a Reading\'s `tab` names one of them. |\n| `list.tab-totals` | A Tab\'s `totals` name a child linked to the List\'s entity by a one-row link (a child linking the List\'s entity twice cannot be totalled), a date of the child, never a moment or a span, and 1 to 6 columns each under its own alias and label; a column\'s `value` is a number of the child, its `where` holds over the child\'s selects and yes/nos, and a value read in each row\'s own unit or currency stands only where that select of the child looks up, through its link, a single select of the List\'s entity, and the child links to no Book\'s rows. A Book\'s rows are totalled by the child\'s one link to them, by one field to one row; a Book the child links to not at all totals nothing on its rows (`app.book-lacks`). Only a table List has them, and no Tab states `totals` beside `dated` or `open`. |\n\n<!-- generated:end node-tab -->\n\nA `totals` Tab adds, per column, the child\'s rows `before` the period\'s first day, `in` the period, or `through` its last\nday, so `before` plus `in` of one value is its `through`:\n\n```json\n{\n "type": "Tab",\n "key": "items",\n "label": "Items",\n "totals": {\n "rows": "ledger_line",\n "on": "moved_on",\n "columns": [\n { "alias": "opening", "label": "Opening", "value": "quantity", "window": "before" },\n { "alias": "received", "label": "Received", "value": "quantity", "where": { "kind": ["receipt"] }, "window": "in" },\n { "alias": "issued", "label": "Issued", "value": "quantity", "where": { "kind": ["issue"] }, "window": "in" },\n { "alias": "closing", "label": "Closing", "value": "quantity", "window": "through" }\n ]\n }\n}\n```\n\n## Field node\n\nA Field draws one field of the record it stands in. How it is drawn follows from the field\'s type and the table\'s facts:\n\n- a control or a value, with its notation, money, units and deadline countdown;\n- a files field as pictures and documents, a long or markdown text as prose written in place \u2014 a gallery or a prose block,\n with a heading (`label`) and a condition (`when`) of its own; a run of files Fields is one gallery;\n- a link as its row\'s chip, with a picker narrowed by `options_where`, and `add` for the new row the picker offers;\n- the back side of a child\'s link as the rows filed under the record, added to with `add`;\n- the fields an Act\'s `sign` writes as one signature.\n\n<!-- generated:start node-field -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field\'s alias |\n| `edit` | `false` | no | false: the field is drawn and read; absent, it is written where the table lets a person write it |\n| `label` | text | no | The heading of the field\'s gallery or prose block, where it draws as one; absent, the field\'s label. A run of files fields takes the first one\'s |\n| `block` | boolean | no | true: a files or text field draws as its own gallery or prose block; false: as a control among the part\'s fields. Absent, files and markdown fields draw as blocks and plain text as a control |\n| `when` | [condition](#app-tree) | no | A gallery or prose block is shown only while the record\'s selects hold these options, its yes/nos these values, its numbers and dates meet their bounds and its `{ "empty": true \\| false }` fields are blank or filled |\n| `add` | `false` \\| `true` \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The new row this field\'s picker offers asks these fields; `true`: the target\'s required fields, its natural key and its title. Stated once per target table, and it holds in every picker of that table in the app. `false`: this link only picks, in every picker of it the app draws, not only where it is written |\n| `record` | list of alias (at least one) | no | The fields of the row this link names that this app saves from the link\'s card, which opens the row in the drawer: stored fields of the linked table a person writes. Stated once per linked table, and it holds on every card of that table in the app, where no List of the app draws its rows. Absent, the card reads the row |\n| `remove` | `true` | no | true: the row this link names is deleted from the link\'s card. Stated once per linked table, with its `record`, and it holds on every card of that table in the app. Absent, the card deletes nothing |\n\n<!-- generated:end node-field -->\n\nA field drawn in the head (title, subtitle, picture, figure, stage) is never also a Field node. A Field is drawn once per\npage. **Writes:** the field, under \xA7 App tree, and with `add` the target table\'s create.\n\n## Act node\n\nAn Act is a verb, stated whole where it stands. Its place decides what it works on:\n\n- in a Section, Side or Page, or in a one-part Record: one record, at that part\'s foot;\n- in a List: on each of its rows, offered in the row\'s \u22EF and, once the row is open, in its header\'s \u22EF; in a child List,\n the child\'s rows;\n- in a composed page\'s `Rows` (\xA7 Composed page): on each row of the child it lists, as in a child List;\n- in a List with `on`: the ticked rows (`"ticked"`) or every row in view (`"view"`), each checked before any is written;\n- directly in a Record that holds parts: refused.\n\n<!-- generated:start node-act -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The act\'s name; its workflow is `act_<key>`, or the `workflow` it names |\n| `label` | text | yes | The verb, as the reader says it |\n| `on` | `"ticked"` \\| `"view"` | no | In a List: the ticked rows, or every row in view; absent, one row |\n| `when` | [condition](#app-tree) | no | The act works only while each named select holds one of these options, each yes/no this value, each number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and each `{ "empty": true \\| false }` field is blank or filled |\n| `requires` | list of alias (at least one) | no | Fields that must be filled first, each drawn where the act stands; a blocked act names each one missing |\n| `recommends` | list of alias (at least one) | no | Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank |\n| `asks` | list of (alias \\| `{ "field": alias, "starts": "today" \\| "me" }` \\| [Asked input](#asked-input)) (at least one) | no | What the reader states when pressing it: fields of the row (written by the act) or inputs |\n| `set` | map of alias \u2192 [value](#app-tree) | no | What the act writes; each field is the act\'s, read-only elsewhere in the app |\n| `workflow` | alias | no | The workflow the act makes and runs under this name, for what `set` cannot say; no Workflow node is declared for it, and `set_app_workflow` sets its body |\n| `writes` | list of alias (at least one) | no | With `workflow`, fields of the acted row its body writes (an id kept from a service\'s answer) \u2014 the act\'s, as what `set` states is |\n| `template` | alias | no | A document template the act makes a file from: an html one made a PDF, an excel one a workbook, a word one a .docx; over several rows, one file of them all |\n| `templates` | list of [Act template](#act-template) (at least 2) | no | Several papers the act makes into `into`, instead of one `template`: the reader ticks which to make, one press making those |\n| `into` | alias | no | The record\'s files field the made document is kept in; remade, it replaces its own last paper, never a person\'s file |\n| `paper` | text | no | Its paper, `<child>.<kind>`: the line of the record\'s papers it stands on |\n| `intake` | alias | no | A child the record lists whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child |\n| `fills` | list of text (at least one) | no | With `intake`, what the papers read may fill: a field of the record, `<link>.<field>` on the row a required one-row link names, or a child whose rows the papers add. With `record`, fields of the new row an agent fills from the transcript |\n| `record` | [Recording](#recording) | no | The press records a call, visit or meeting and files it as a new row of a child the record lists |\n| `sign` | [Signature](#signature) | no | The press opens a pad the signer signs on; the act keeps the signature as a picture with who signed, when and the witness |\n| `open` | text | no | The id of another app that reaches this row\'s table, opened at this row by the press: navigation only \u2014 no workflow runs, and a reader without `use` on it is shown that app\'s own refusal. Stated alone with `label` and `when` |\n| `review` | `true` | no | With `workflow`: the workflow only proposes a changeset of the act\'s table, and the reader ticks the rows to write |\n| `confirm` | `true` \\| text | no | The press asks first: the question, or `true` for the act\'s label over the row |\n| `danger` | `true` | no | The act destroys or cannot be undone |\n\n#### Asked input\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `input` | alias | yes | A value the act asks for that is not a field of the record |\n| `label` | text | yes | What the reader is asked, in their words |\n| `type` | `"text"` \\| `"long_text"` \\| `"number"` \\| `"date"` \\| `"select"` \\| `"link"` \\| `"member"` \\| `"file"` | yes | What the reader states. A `file` is one uploaded file, handed to the act\'s authored `workflow` as its file id and read by nothing else |\n| `options` | list of [Select option](#select-option) (at least one) | no | The choices a `select` input offers, one picked \u2014 written into a select holding the same option aliases |\n| `entity` | alias | no | The entity a `link` input picks one row of |\n| `accepts` | list of `"sheet"` \\| `"pdf"` \\| `"document"` \\| `"photo"` (at least one) | no | The kinds of file a `file` input takes; absent, a sheet, a PDF or a document \u2014 a phone offers its camera only where `photo` is among them |\n| `required` | `true` | no | The act is refused without it |\n| `default` | alias | no | A field of the row whose value the input starts from, still changed at will |\n| `add` | `true` \\| list of (alias \\| [Field](#field-node)) (at least one) | no | The new row this picker offers asks these fields; `true`: the target\'s required fields, its natural key and its title. Stated once per target table, and it holds in every picker of that table in the app |\n\n#### Set formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | text | yes | A formula over the acted row\'s fields, `{alias}` each, read as the entity\'s own formulas read them |\n\n#### Act template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `template` | alias | yes | A document template the act makes a paper of, kept in `into` \u2014 an html one made a PDF, an excel one a workbook, a word one a .docx |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked |\n\n#### Recording\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | A child the record lists in a timeline or rows block, linking back to it by a single link: each recording is filed as a new row of it |\n| `audio` | alias | yes | The child\'s files field the recording\'s audio is kept in |\n| `transcript` | alias | yes | The child\'s text field the transcript is kept in, a speaker\'s turn per line |\n| `video` | alias | no | The child\'s files field the screen is kept in, where the recording captured it |\n| `at` | alias | no | The child\'s date field stamped with the moment the recording started |\n| `by` | alias | no | The child\'s member field set to the person who recorded |\n\n#### Signature\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | The record\'s files field the signature is kept in, as a picture \u2014 each signing replaces the one before |\n| `by` | alias | yes | Who signed: a text field the signer types their name into (someone with no seat in the workspace \u2014 a driver, the owner\'s supervisor), or a member field set to the person pressing, who signs as themselves |\n| `at` | alias | yes | The record\'s date field stamped with the server\'s time as the act runs |\n| `witness` | alias | no | Beside a text `by`, the member field set to the person pressing, who saw the signer sign |\n\n<!-- generated:end node-act -->\n\nAn Act whose `when` does not hold is not offered. One whose `requires` are empty is offered disabled, naming each; one whose\n`recommends` are empty names what it will leave blank and still presses; one a standing Check blocks is offered disabled in\nthe check\'s words. The generated `act_<key>` workflow re-checks all three on the server, writes `set` and what was asked,\nand appends the status history where the act moves the status. A field an act asks is read-only everywhere else. A `select`\ninput offers its own options and is written into a select holding the same option aliases; a `link` input picks one row of\nits `entity`, a `member` input one person. A `file` input is one uploaded file handed to the act\'s authored `workflow` as its\nfile id; nothing else reads it. A field an act both `requires` and `asks` never disables the press.\n\n- **Writes:** every field it sets, asks, files into, signs or records is the act\'s.\n- **Papers.** A button that makes a paper is `template` (or `templates`) with `into` on the Act, with no workflow. `template` makes a document from the row as the act left it and keeps it in `into`; over several rows it\n makes one document of them all. `templates` names a set of papers kept in `into`, each made one as its file and each not\n made yet as a placeholder, those whose `when` holds ticked to start; the press makes the ticked ones, each replacing the\n file its template made before, and the set downloads as one archive (`archive_<key>`). `intake` reads papers: the press\n takes files, an agent reads them, and each is filed as the record\'s line of its kind in the named child; what they state\n of each field `fills` names is shown beside what the row holds, and the save writes only what differs. `sign` opens a pad\n on the device: the act keeps the picture in `into` in place of any before it, `at` stamped with the server\'s time and the\n person pressing in `witness`; the record reads `into` as one signature, and the act\'s paper is handed the picture under\n `into`\'s name (`{{#each signature}}<img src="{{url}}">{{/each}}` in an html template). `record` records a call or a visit\n through the host and files its audio, transcript and screen as a new row of a child.\n- **`workflow`** is an authored workflow (`workflows`) the act runs after its checks, for what neither `set` nor `template` can say.\n **`review: true`** makes it one that only proposes: its body reads, returns a changeset of the act\'s table in\n `data.review`, and the platform writes the rows the reader ticks in the act\'s dialog, each checked first and all written\n in one transaction or none. Over every row in view the act is handed no rows.\n- **A `when` that compares** splits one stage by an amount or a date, with no formula field behind it. The server refuses a\n press the row\'s numbers and dates do not meet, in the words "Approve works only while Amount is at most 5,000,000", and\n a blank number or date meets no bound. Two acts on an expense claim, the second moving it to a stage the Director group owns\n (stage owners decide who presses):\n\n```json\n{ "type": "Act", "key": "approve", "label": "Approve", "when": { "status": ["submitted"], "amount": { "lte": 5000000 } }, "set": { "status": "approved" } }\n```\n\n```json\n{ "type": "Act", "key": "send_to_director", "label": "Send to the director", "when": { "status": ["submitted"], "amount": { "gt": 5000000 } }, "set": { "status": "director_review" } }\n```\n\n```json\n{ "type": "Act", "key": "gate_in", "label": "Make the gate-in EIR", "template": "eir_in", "into": "eir", "sign": { "into": "driver_signature", "by": "driver_name", "at": "signed_at", "witness": "signed_before" } }\n```\n\n## Check node\n\nA Check is a yes/no or text formula of the rows of the List it stands in: it stands while true or non-empty, and a text\nformula\'s words are what the reader sees. Without `on` it only warns; with it, it refuses the saves and acts it names, on\nthe server, in every write it names.\n\n<!-- generated:start node-check -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A yes/no or text formula of the rows of the List it stands in: true or non-empty means the check stands |\n| `on` | `"all"` \\| list of (alias \\| `"update"` \\| `"delete"`) (at least one) | no | What the standing check refuses: `update` is the rows\' saves, an Act key is that act, `all` is every act and save; absent, it only warns. `delete` is this app\'s removal of the rows, for a rule no check of the table states |\n| `resolve` | alias | no | An Act on the same rows that clears it |\n\n<!-- generated:end node-check -->\n\n`update` is the rows\' saves; an Act key is that Act, on the same rows; `all` is every act and save.\nA rule that refuses a Delete is a check of the table, not a Check: state it in the table\'s `checks` with `on: ["delete"]`\n(\xA7 Write rules), where it holds for every way a row is deleted and a removal that cascades to child rows is refused whole.\nA Check naming `delete` refuses only this app\'s removal of a row, and is kept for a rule the table\'s checks cannot state (a\nformula that is neither a yes/no nor a text).\nA Check in a child List stands on each child row. A warning reading the figure of a meter that `gates` marks against its\nbound or its pass mark is refused: the meter already draws it.\n\nThe check field\'s `label` is its short name \u2014 "Over budget", "Release expired": a row\'s line says it in the check\'s tone\n(the most severe of several, and how many more), while its words whole are the row\'s tooltip. Label it as the reader names\nthe problem, in a few words, and let a text formula say the sentence. A check with no `resolve` links to the first field its formula reads that the record draws.\nA yes/no formula whose yes needs attention (papers the customer still owes) is a Check: its yes reads in the warning tone\nwherever it is drawn, every other yes in the success tone \u2014 and the row\'s line says it, so it is no column of the List too.\n\nA Check that every app over a table states the same way restates the table\'s contract and belongs in the table\'s `checks`\n(\xA7 Write rules).\n\nA Check refusing `update` (or `edit: false` on a record, part or field) makes the rows read-only in this app alone, drawn\nunder the read-only mark, never as a lock. A lock is the table\'s: a table workflow, a person, or an Act whose workflow calls\n`lock_records` locks a row, and every door refuses its writes until a table manager unlocks it \u2014 `unlock_records` in an\napp\'s run is judged as the member who pressed. A locked row reads locked in every app: its lock mark, its fields as values\nunder one line naming who can change it, its saves, Delete and each Act writing it refused \u2014 an Act that writes\nelsewhere, makes a paper it hands back, or unlocks it stays its workflow\'s to answer.\n\n## Reading node\n\nA Reading is a number over rows: one figure, its parts, a grid, a figure per period, steps; or a record\'s own figure. The\npicture is the runtime\'s, chosen from what the reading means \u2014 no prop picks a chart. The question is stated by which\nsplit it names:\n\n| Names | Answers |\n|---|---|\n| nothing | one figure: the rows counted, or `value` summed, `{ "avg": \u2026 }` averaged |\n| `by` | the figure per value of a select, one-row link, member or yes/no, or per band of a number\'s `tiers` |\n| `by` a date field | the figure per period (a trend); `ahead: true` reads forward over the rows still to come |\n| `by` and `across` | a grid, `by` down the side and `across` along the top |\n| `steps` | a funnel: 2 to 8 number fields summed, each kept as a share of the one before; in a record only |\n| no `entity`, in a Record | the record\'s own `value`, a tile under its field\'s label; two to eight consecutive tiles draw as one strip, and a single one is a field of the section |\n\n<!-- generated:start node-reading -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The reading\'s query name, unique in the app; a record\'s own figure is keyed by its field |\n| `label` | text | no | Absent, a split\'s `by` field\'s label, a trend\'s summed `value`\'s (else the rows\'), a grid\'s two fields\'; one figure states its label |\n| `entity` | alias | no | The List\'s rows; in a Record, the child\'s rows under it; absent in a Record with no `by`: the record\'s own `value` |\n| `via` | alias | no | The child\'s link to the record, where it has several |\n| `value` | alias \\| `{ "avg": alias }` | no | A number field summed over the rows, or `{ avg }` averaged; absent, the rows are counted |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, whose number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), whose field is `{ "empty": true \\| false }`, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `by` | alias \\| [Tiered filter](#tiered-filter) | no | Splits the rows into parts; a date field splits them per period |\n| `across` | alias \\| [Tiered filter](#tiered-filter) | no | With `by`: the columns of a grid |\n| `over` | alias | no | The date that places a row in time |\n| `ahead` | `true` | no | With a date `by`: the current period and those after it |\n| `steps` | list of alias (2\u20138) | no | Number fields summed, each a step of a funnel |\n| `on_hand` | [Reading on hand](#reading-on-hand) | no | With `by` (and `across`): the Reading\'s rows are what is owed, per value of those fields; drawn beside what is on hand per the same values, and the shortfall |\n| `target` | number | no | The number the figure is read against; absent, its bound, where the table states one |\n| `better` | `"up"` \\| `"down"` | no | Which way is good, for the figure\'s tone; absent, read from the figure\'s shape |\n| `tab` | alias | no | The Tab it reads, else the open one |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n\n#### Reading on hand\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | The entity whose rows are what is on hand, all of them |\n| `value` | alias | no | A number field summed over those rows; absent, the rows are counted |\n| `by` | alias | no | The field of this entity matching the Reading\'s `by`; absent, the same alias |\n| `across` | alias | no | The field of this entity matching the Reading\'s `across`; absent, the same alias |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, whose number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), whose field is `{ "empty": true \\| false }`, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n\n<!-- generated:end node-reading -->\n\nWhere a Reading stands decides which rows: a dashboard\'s every row, a List\'s the rows in view (so the search, the stage and\nthe filters narrow each one), a Record\'s the child\'s rows under the record. A Reading counts its rows, or sums the number\n`value` names \u2014 never a percent. `over` is a date, the row\'s own or its parent\'s through a lookup: a trend\'s periods, a\nfigure\'s recent periods, and what a dashboard\'s period windows. `where` keeps rows whose select holds one of the options, or\nwhose yes/no is the value, every entry ANDed \u2014 a band leads with the job\'s exception as a formula\'s yes/no (what is\nshort), never a stage the chips already count nor the overdue rows the register\'s summary line counts from `records.due`.\n\n- **A figure\'s change** is green where it moves the good way: a sum\'s rise, a count kept by `where` falling; `better` states\n the other way where that reads wrong (money going out). A value `limits` bounds reads against the sum of its bound\n (`10 / 14`); `target` reads one against a number nothing bounds.\n- **An average** is its sum over the rows that hold a value, divided by how many hold one; a row holding none is no part\n of it, and where none holds one the figure reads a dash, never 0. Under a dashboard\'s period it is the average over the\n period so far, never a sum of averages. A share is an average too: a number formula that is `100` on the rows that count\n and `0` on the others, in percent notation, averaged. Only a single figure averages.\n- **Bands.** `by` and `across` each take, in place of a field, a number by its `tiers` \u2014 `{ "field", "tiers" }` \u2014\n splitting the rows by the bands those breakpoints cut: `[1, 31, 61]` over days late reads not due, 1\u201330, 31\u201360 and over\n 60, the aged report. Every band is drawn in the bands\' own order, one holding no row at zero; rows holding no number\n stand apart. On a List a band is a door: a press narrows the rows to it. In a List\'s band, a split by one or two\n breakpoints stands in it, and one by three or more stands whole under it.\n- **In a List,** Readings stand under the title in one compact band: the job\'s headline number first, then at most the mix\n and the movement. A picture stands only where it splits the rows in view into two parts or more, one holding two rows or\n more. The summary line already totals the figure and the chips count the rows, so a figure summing the figure, or counting\n the rows of a table with a stage, is refused unless its `where` narrows it.\n- **Name.** A Reading\'s `key` is the query it is read by (`run_app_query`).\n\n## Progress node\n\nProgress reads one parent\'s work against its plan \u2014 a List\'s band over a folder, or a record\'s block, never a dashboard\'s\nevery row: the share of the work planned and the share done, cumulative day by day, today\'s two at its foot. Each row is a\npart of the work, its plan spread evenly over its planned days and weighing its `weight` (its value) or, absent, its planned\ndays. Its share done today is `done`, a percent the server computes; on an earlier day, its `done` from its `finished` day\non, before it what its `measured` rows add up to by then.\n\n<!-- generated:start node-progress -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The reading\'s query name, unique in the app |\n| `label` | text | yes | What the progress is of, in the reader\'s words |\n| `entity` | alias | yes | The rows that are the parts of the work: the List\'s, or in a Record the child\'s rows under it |\n| `via` | alias | no | The child\'s link to the record, where it has several |\n| `planned` | [Progress planned](#progress-planned) | yes | The days each row\'s work is planned over, its share planned spread evenly across them \u2014 a row missing either is no part of the plan, and neither line counts it |\n| `done` | alias | yes | A percent field of the row the server computes: its share done today (0\u2013100) |\n| `finished` | alias | no | A date of the row: the day its work was finished, from which it stands at its `done` |\n| `weight` | alias | no | A number of the row: what it weighs in the whole; a row holding none weighs nothing. Absent, each row weighs its planned days |\n| `measured` | [Progress measured](#progress-measured) | no | Before a row is finished, its share done on a day is what these add up to by then, never past its `done`; absent, a row stands at nothing until it is finished, and at its `done` today |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, whose number or date meets its bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is read when the rows are), whose field is `{ "empty": true \\| false }`, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `tab` | alias | no | The Tab it reads, else the open one |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n\n#### Progress planned\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of the row: the first day of its planned work |\n| `end` | alias | yes | A date of the row: the last day of its planned work |\n\n#### Progress measured\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows record the work as it is done, each through its one-row link to a row of the reading |\n| `on` | alias | yes | A date of those rows (their own, or a lookup): the day each records its work |\n| `share` | alias | yes | A number of those rows the server computes: the share of its row each adds, in percent |\n\n<!-- generated:end node-progress -->\n\n## Statement node\n\nA Statement draws a figure the record works out from its own figures \u2014 a claim\'s payable, an invoice\'s total, a payslip\'s\nnet \u2014 line by line in the order the arithmetic runs: each line a number of the record, the first stating no `op`, the last\nan `equals`. Every figure is a field the server computes or a person writes; the screen adds nothing up. A line\'s `parts`\nlists the rows its figure carries, each read from its own row, and that figure is what they add up to \u2014 a formula, rollup\nor lookup, or the server\'s copy when the record is made (`default_from`), which nothing writes after.\n\n<!-- generated:start node-statement -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | The statement\'s name, unique among its siblings |\n| `label` | text | no | Absent, the label of what the statement comes to |\n| `lines` | list of [Statement line](#statement-line) (at least 3) | yes | The figures it is built from, top to bottom, ending in the one it comes to |\n| `when` | [condition](#app-tree) | no | Drawn only while the record\'s selects hold one of these options, its yes/nos this value, its numbers and dates meet their bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }` is today) and its `{ "empty": true \\| false }` fields are blank or filled |\n\n#### Statement line\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number of the record \u2014 stored, a formula, a rollup or a lookup \u2014 read on the line after its label |\n| `op` | `"plus"` \\| `"minus"` \\| `"times"` \\| `"equals"` | no | How the line works on the lines above it \u2014 `plus`, `minus`, `times`, or `equals` for what they come to, a formula, rollup or lookup the server computes; absent on the first line, which the rest work from |\n| `rate` | alias | no | A percent of the record read after the line\'s label: the rate its figure is taken at (retention at 5 %) |\n| `parts` | [Statement parts](#statement-parts) | no | The rows the line\'s figure carries, each listed under it with its words and its own figure \u2014 read from those rows, never a copy of them. The line\'s figure is what they add up to, drawn read: a formula, rollup or lookup, or a value the server copies when the record is made (`default_from`) that nothing writes after |\n\n#### Statement parts\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows the line\'s figure carries \u2014 the record\'s own for the earlier rows of the same thing |\n| `same` | alias | yes | A one-link the record and those rows both hold: the rows naming what the record names in it |\n| `at` | alias | no | A moment (a datetime) of those rows no person keys \u2014 when each was certified: with `before`, only the rows whose moment stands before the record\'s, earliest first; absent with it, every row so named, in their own order |\n| `before` | alias | no | A moment (a datetime) of the record no person keys \u2014 when it was made: the moment the rows\' `at` stands before; stated with `at` |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| [comparison](#values) \\| `{ "empty": true \\| false }`) | no | Only the rows whose single select holds one of these options, whose yes/no (stored, or a formula) is this value, whose number or date meets its bounds, or whose field is empty or filled \u2014 read so on the server |\n| `columns` | list of alias (1\u20132) | yes | The rows\' fields read as each one\'s words, in order (its date, its percent) |\n| `figure` | alias | yes | The rows\' number each one reads at the figures\' edge, frozen when the row was certified: a stored number an act fills (`{ "formula": \u2026 }` as it runs) or the server copies when the row is made (`default_from`) \u2014 never one a person keys, nor a formula, rollup or lookup the server recomputes |\n\n<!-- generated:end node-statement -->\n\n```json\n{\n "type": "Statement",\n "key": "claim",\n "lines": [\n { "field": "contract_value" },\n { "field": "done_to_date", "op": "times" },\n { "field": "value_to_date", "op": "equals" },\n { "field": "certified_before", "op": "minus", "parts": { "rows": "claim", "same": "contract", "at": "certified_at", "before": "made_at", "where": { "kind": ["work"] }, "columns": ["claimed_on", "done_to_date"], "figure": "certified_value" } },\n { "field": "claim_value", "op": "equals" },\n { "field": "retention", "op": "minus", "rate": "retention_rate" },\n { "field": "payable", "op": "equals" }\n ],\n "when": { "kind": ["work"] }\n}\n```\n\n## Export node\n\nAn Export is a report file of the rows in view: the title, a line per chip, the search and the moment it was exported, the\nReadings, and every row, at most 20,000. With no props, the one Export is the workbook of the List\'s columns, the Readings\non a sheet before the rows. A List states the reports the business sends \u2014 one a button, several one menu \u2014 each a `label`,\na `description`, and a `template` filled with `title`, `lines`, `readings`, `at` (the moment it was made), `dates` (each\ndate filter\'s `from` and `to` by field) and the rows under the table\'s alias (an html one made a PDF, an excel one a\nworkbook). A report\'s `filename` names its file in the template grammar over the same keys but the rows. Where `per` is a\none-row link, `per_row.<field>` reads a text, number, date or yes/no field of the picked row, and `| add_days:<n>` shifts a\ndate by whole days (`n` a number or such a field, empty for none): `Stock {{per}} {{period.to | add_days:per_row.day_offset | format:"dd.MM.yyyy"}}`. A report of some\nrows (`where`), or of one value the reader picks (`per`: one file per value), is made of the List as those chips would\nnarrow it; the screen keeps the chips and rows it shows.\n\n<!-- generated:start node-export -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `key` | alias | yes | Unique among its siblings; part of the node\'s address |\n| `label` | text | no | The report\'s name; absent on the one Export that stands for the register\'s workbook |\n| `description` | text | no | What the report is for, in a sentence |\n| `template` | alias | no | An html or excel document template the report fills, with `title`, `lines`, `readings`, `at`, `dates`, `period`, `per` and `per_row` (a one-row `per`\'s picked row, the fields `filename` reads); absent, the workbook of the List\'s columns |\n| `filename` | text | no | The file\'s name, at most 500 characters, in the template grammar, over one value each: `title`, `at`, `dates.<field>.from` or `.to`, `period.from` or `.to`, `per`, `per_row.<field>` (a text, number, date or yes/no field of the row a one-row `per` picks), `lines \\| lookup:<n>`; `\\| add_days:<n>` shifts a date by whole days, `n` a number or `per_row.<field>`. Absent, the report\'s label |\n| `where` | [narrowing](#app-tree) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`{ "me": true }`) \u2014 every entry ANDed |\n| `per` | alias | no | A select, member or one-row link the reader picks one value of: one file of that value |\n| `books` | list of alias (at least one) | no | Of the List\'s books, the tables the report reads; absent, all |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.export-report` | A List\'s Exports each have their own `label`; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes. |\n| `list.export-template` | A template an Export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the List\'s entity alias \u2014 an entity named none of those \u2014 and is the document of no Act. |\n\n<!-- generated:end node-export -->\n\n## Book node\n\nA Book is another table read as part of the register: the same job over its rows \u2014 a legacy ledger, a second branch\'s book.\nA row of the book opens the List\'s Record, lined up by alias; a field the book lacks is blank on its rows and drops from\nwhat they open, and an Act, Check or add needing it is not offered on them. Options line up by alias. The Book\'s own Acts\nand Checks, in its own fields, stand beside the List\'s, each Act\'s `key` its own among the App\'s: an Act of the List running\nits own `workflow`, papers or a call never runs on a book\'s rows, so the Book states its own. With `column: true` the List\ndraws a column naming the table each row is of, and a filter by it. A Book holds the title, the stage, the scope and every\nCheck locking the rows; a table, cards or a plan reads Books.\n\n<!-- generated:start node-book -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | Another table read as part of the register: the same job over its rows |\n| `fields` | map of alias \u2192 alias | no | A field of the List\'s entity \u2192 the field of this entity holding the same fact, where their aliases differ |\n| `column` | `true` | no | The List draws a column naming the table each row is of, and a filter by it |\n| `row` | [Book row](#book-row) | no | The book\'s rows, stated where they differ from the List\'s |\n| `children` | list of ([Record](#record-node) \\| [Act](#act-node) \\| [Check](#check-node)) (at least one) | no | The Fields a save of the book\'s rows writes (and `edit: false` for none), in a Record as a child List\'s Record states them \u2014 the page a row opens is the List\'s; and the book\'s own Acts and Checks over its rows, beside the List\'s its rows can run |\n\n#### Book row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `status` | `{ "edit": true }` | no | `{ edit: true }`: the book\'s stage is also moved by hand |\n\n<!-- generated:end node-book -->\n\n**Writes:** the List\'s `remove`, its Fields and its Acts, on the book\'s own table.\n\n## Query node\n\nA Query is a read for agents, never drawn. An app generates a query for everything its screen reads (`agent_surface` lists them); a Query declares one the screen never asks, called through `run_app_query` or the app\'s API: the\ntable whose rows it reads, the fields each row returns or the figure they add up to, and what narrows them.\n\n<!-- generated:start node-query -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `description` | text | yes | What the query answers, read by whoever calls it by its alias \u2014 at most 300 characters |\n| `params` | map of alias \u2192 [Workflow value](#workflow-value) | no | What a call states, by name, each read by an entry of `where` \u2014 a `number`, `boolean`, `date`, `select`, `record_link`, `member`, holding one. `required: false`: the call may leave it out, and the condition reading it narrows nothing |\n| `rows` | alias | yes | The entity whose rows it reads, under that entity\'s read scope. The app\'s own entity is read as its list reads it: kept by the register\'s `where`, and inside the row the app\'s `scope` names, which the call states |\n| `fields` | list of alias (at least one) | no | The fields each row returns, by alias. A query states `fields` or `reading`, one of the two |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order the rows return in; absent, the order the entity\'s list reads in. Only with `fields` |\n| `limit` | integer | no | The most rows returned. Only with `fields` |\n| `reading` | [Query reading](#query-reading) | no | The rows counted or summed instead of returned: one row for all of them, or one per value of `by` or per day of `over`, each carrying its `by` field or `__day`, `__count`, and `__sum` where `value` is stated |\n| `key` | alias | yes | The query\'s name, what `run_app_query` calls; unique in the app |\n| `where` | map of alias \u2192 [condition](#query-node) | no | What narrows the rows, as a List\'s `where` does: a select\'s options, a yes/no\'s value, a number or date meeting its bounds (`gt`, `gte`, `lt`, `lte`), a field `{ "empty": true \\| false }`, `{ "me": true }` on a member field \u2014 or a value the call states (`{ "param": name }`), or a date or number range of params (`{ "from": \u2026, "to": \u2026 }`) |\n\n#### Query reading\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `average` | `true` | no | The `value` is averaged: summed over the rows holding one, beside how many hold one. Never with `by` or `over` |\n| `by` | alias | no | The single select, one-row link, member or yes/no each row is counted under \u2014 one row returned per value |\n| `over` | alias | no | A date: one row returned per day of it. Never with `by` |\n\n#### Query param\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `param` | text | yes | A param of this query: the condition holds what the call states for it, and is left out where the call states none |\n\n#### Query range\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | [Query param](#query-param) | no | The param the field is on or after \u2014 a date\'s first day, a number\'s least |\n| `to` | [Query param](#query-param) | no | The param the field is on or before \u2014 a date\'s last day, a number\'s most |\n\n<!-- generated:end node-query -->\n\n```json\n{\n "type": "Query",\n "key": "fees_unbilled",\n "description": "A customer\'s fees no invoice covers yet, oldest first.",\n "params": {\n "customer": { "type": "record_link", "entity": "customer", "description": "The customer whose fees are read." },\n "since": { "type": "date", "required": false, "description": "Only fees charged on or after this day." }\n },\n "rows": "fee",\n "fields": ["visit", "service", "amount", "charged_on"],\n "where": { "billed": false, "customer": { "param": "customer" }, "charged_on": { "from": { "param": "since" } } },\n "sort": { "field": "charged_on" },\n "limit": 200\n}\n```\n\nAn entry of `where` holds what a Reading\'s `where` holds, or a value the call states: `{ "param": "<name>" }`. A required\nparam is always stated; one stating `required: false` may be left out, and the condition reading it then narrows nothing.\nA query reads under its table\'s read scope, as every read of the app does. A `reading` returns one row for all the rows it\nreads, or one per value of `by` or per day of `over`. `set_app` compiles a Query with the rest of the app: no code is set.\n\nEvery Reading of the app is a query too, returning its groups, called by its key. A Tab\'s `totals` read is called by\n`<table>_totals_<tab>` with the period\'s two days, `from` and `to`, both required.\n\nA query reads one table: on an app that states `Book`s, whose list reads several, it names a book\'s own table, never the\nList\'s. A param compares a field holding one value \u2014 or, as a range, a date or a number \u2014 and never a number read in each\nrow\'s own unit or currency, nor a date holding a span.\n\n## Workflow node\n\nA Workflow is a verb for agents, never drawn: a job no press of the screen starts \u2014 an agent or a service handing the app a\nfile, or a list of rows to reconcile a table against. It states what it takes and what it hands back; a table is named by\nits alias in `tables` and a select by `<table>.<field>`. It is called through the app, by `run_app_workflow` or the app\'s\nAPI. On an app shared by a public link it runs only for someone who holds the app, and a visitor\'s call is refused as an\nalias the app does not have.\n\n<!-- generated:start node-workflow -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `description` | text | yes | What the workflow does, read by whoever calls it by its alias \u2014 at most 300 characters |\n| `inputs` | map of text \u2192 [Workflow value](#workflow-value) | no | What a call hands it, by name \u2014 checked before its body runs |\n| `outputs` | map of text \u2192 [Workflow value](#workflow-value) | no | What its body returns as `data`, by name \u2014 checked as it returns; absent, read off the body |\n| `key` | alias | yes | The workflow\'s name, what `run_app_workflow` calls; unique in the app |\n\n#### Workflow value\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"boolean"` \\| `"date"` \\| `"datetime"` \\| `"email"` \\| `"json"` \\| `"record_link"` \\| `"select"` \\| `"member"` \\| `"file"` \\| `"object"` \\| `"array"` | yes | What the value is |\n| `description` | text | no | What the value is for, read by whoever calls the workflow |\n| `required` | `false` | no | false: the value may be left out; absent, it is always there |\n| `multi` | `true` | no | A `record_link`, `select`, `member` or `file` holding several |\n| `entity` | alias | no | The entity a `record_link` names a row of |\n| `field` | text | no | `<entity>.<field>`: the select field whose options a `select` takes, as the field holds them when the workflow runs |\n| `options` | list of alias (at least one) | no | The values a `select` takes, where no field holds them |\n| `fields` | map of text \u2192 [Workflow value](#workflow-value) | no | An `object`\'s values, by name |\n| `items` | [Workflow value](#workflow-value) | no | What each item of an `array` is |\n\n<!-- generated:end node-workflow -->\n\n```json\n{\n "type": "Workflow",\n "key": "reconcile_fees",\n "description": "Reconciles a visit\'s fees against the lines of the carrier\'s statement.",\n "inputs": {\n "visit": { "type": "record_link", "entity": "visit" },\n "statement": { "type": "file", "required": false },\n "lines": { "type": "array", "items": { "type": "object", "fields": { "service": { "type": "select", "field": "fee.service" }, "amount": { "type": "number" } } } }\n },\n "outputs": { "added": { "type": "number" }, "changed": { "type": "number" } }\n}\n```\n\nIts code is set on the app, never stated in the tree: `set_app_workflow` with the app, the key and the `source`, leaving\n`inputs` and `outputs` out \u2014 the code is checked against what the node declares. Every `set_app` keeps that code and holds\nwhat it takes, what it hands back and its description to the node. To change what a workflow takes or hands back, change\nthe node first: `set_app` keeps the code where it still meets the change, and where it does not the workflow stands on no\ncode again until `set_app_workflow` sets one that does. With no code it refuses every call, and each `set_app` says so. A\nWorkflow the tree stops declaring is archived with its key.\n\n## Composed page\n\nA Record whose page is its own arrangement of what the app already states \u2014 a piece of writing under its title, a brief\nbeside its figures \u2014 states `compose` instead of parts: a tree of finished pieces, each named by `use`. A `Rows` holds\nthe Acts and Checks of its rows as `children`, each named by `type`: they are the app\'s own acts and checks on those rows,\nnever pieces of the page.\n\n```json\n{\n "type": "Record",\n "door": "page",\n "compose": {\n "use": "Column",\n "children": [\n { "use": "Bar", "children": [{ "use": "Status" }, { "use": "Field", "field": "audience" }, { "use": "Act", "act": "send_for_review" }, { "use": "Copy", "field": "body", "as": "plain" }] },\n { "use": "Title" },\n { "use": "Document", "field": "body" },\n { "use": "Group", "title": "Reviews", "children": [{ "use": "Rows", "child": "review", "create": false, "children": [{ "type": "Act", "key": "resolve_review", "label": "Resolve", "workflow": "resolve_review", "confirm": true }] }, { "use": "Act", "act": "ask_reviewer" }] }\n ]\n }\n}\n```\n\nA node binds by name: a `field` is a field of the record\'s table, an `act` an Act of the app, a `child` a table linking to\nthe record. What a name binds is read and written exactly as a part naming it would be \u2014 a `Field` and a `Document` are\nsaved through the record\'s own save, an `Act` runs its workflow, a `Rows` reads the child\'s rows \u2014 so a piece adds no write\nof its own; only an Act inside a `Rows` declares one, as an Act of a child List does. A piece holds no condition and no computed value: a piece that does not apply on a row draws nothing, and anything\nthat must depend on the row is an Act\'s `when` or a field\'s formula, never the tree.\n\nThe first `Act` the page itself draws is its primary and every other one secondary; a `danger` act reads so wherever it\nstands. A `Copy` and a `Document` naming one field copy what is on screen \u2014 the words typed and not yet saved among them. The\nrecord\'s chrome stays the runtime\'s: the way back, its comments door and its \u22EF with Delete share the row over the tree, and the\nhead draws no title and no status of its own \u2014 the tree\'s `Title` and `Status` do.\n\n<!-- generated:start apps-compose -->\n\n#### Column piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Column"` | yes | The page\'s reading column, 680px: its pieces stacked in order from one left edge |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces stacked in it, in the order they are read |\n\n#### Bar piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Bar"` | yes | One row of small things on one control height, in two zones: what the record is \u2014 a status, a field\'s control \u2014 from its left edge, what is done to it \u2014 an act, a copy \u2014 ending on its right edge, taking a row of their own under it where the row runs out of room |\n| `children` | list of [piece](#composed-page) (at least one) | yes | Its pieces, each zone\'s in order; a `Field` directly in it is its control alone on one line, named by its label where it holds nothing; an `Act` that destroys or corrects is not drawn here \u2014 it stands in the record\'s \u22EF |\n\n#### Group piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Group"` | yes | A titled stack, headed once by `title` |\n| `title` | text | yes | Its heading, in the reader\'s words; holding one `Rows`, it carries their count and their add, and the group\'s `Act`s end its line |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces under the heading, in order |\n\n#### Panel piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Panel"` | yes | What a reader does not need while writing: nothing in the page, one entry of the record\'s \u22EF named by `title` that opens its pieces in a panel beside the page \u2014 a sheet on a phone; directly in the root `Column` alone |\n| `title` | text | yes | Its entry in the \u22EF and the panel\'s name, in the reader\'s words; an `Act` directly in it ends that name\'s row |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces in the panel, in order: `Rows`, `Field`, `Act`, `Group`, `Document` \u2014 never `Title`, `Status`, `Bar`, `Copy` or another `Panel` |\n\n#### Title piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Title"` | yes | The record\'s title (`records.<entity>.title`) as the page\'s heading, written in place where a person writes it; one a page |\n\n#### Status piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Status"` | yes | The record\'s status (`records.<entity>.status`) as a record\'s head draws it: moved by hand, by its acts\' moves, or read |\n\n#### Field piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Field"` | yes | A field of the record under its label, as a section\'s `fields` draws it |\n| `field` | alias | yes | The field drawn \u2014 never the title or the status, which `Title` and `Status` draw; once a page |\n\n#### Document piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Document"` | yes | A markdown text field written in place with no box, saved as it is written \u2014 whether it is saved said at its foot |\n| `field` | alias | yes | A text field with `format: "markdown"`; once a page |\n\n#### Act piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Act"` | yes | An act of the app on the record as its button: its `when`, what it lacks, its asks and its confirmation as everywhere; the first `Act` the page itself draws is its primary |\n| `act` | alias | yes | An act of this app on the record \u2014 never one `of` a child\'s rows, which stands on its row; once a page |\n\n#### Rows piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Rows"` | yes | A child entity\'s rows under the record, as a rows block lists them, each row\'s own acts on it |\n| `child` | alias | yes | A child entity linking to the record; once a page |\n| `create` | `false` | no | false, rows are not added here \u2014 an act of the page makes them |\n| `remove` | `true` | no | The rows offer a Delete (open row and \u22EF); absent, none |\n| `children` | list of ([Act](#act-node) \\| [Check](#check-node)) (at least one) | no | The acts and checks of its rows, each standing on every row listed, as a child List\'s stand on its rows; absent, the rows offer none |\n\n#### Copy piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Copy"` | yes | A button copying a field\'s words as the page holds them now \u2014 what is typed in its `Document` and not yet saved among them |\n| `field` | alias | yes | The text field copied |\n| `as` | `"plain"` \\| `"raw"` | yes | `plain`, its markdown marks gone; `raw`, as stored |\n\n<!-- generated:end apps-compose -->\n\n## Tasks\n\nA table whose `records` states `"task": true` \u2014 its status a stored select with `closed` \u2014 is work someone finishes.\nEvery List of its rows draws it alike, each row led by a ring. Ticking the ring moves the row to a closed option by the\none write the app already has for that move: the Act of that table setting a closed option, offered on an open row and\nasking nothing, else its own save where people write the status \u2014 stamping what that act stamps and appending the\nstatus history. Unticking runs the Act moving a closed row back, else the save back to the status\'s default. A move no\nwrite reaches leaves the ring disabled and the check notes it. An Act with `on: "ticked"` completes several at once.\n\nA record listing task rows filed under it by a link holding one row says how much of that work is done \u2014 those at a\nclosed option of all \u2014 wherever its row is drawn: a meter on its head, a column of its register\'s rows, a card\'s foot, a\ncalendar entry, a gantt\'s or a lane\'s bar. No rollup is stated for it.\n\n```json\n{\n "tables": [\n {\n "ref": "project",\n "label": "Projects",\n "singular": "Project",\n "fields": {\n "name": { "label": "Name", "required": true, "type": "text" },\n "lead": { "label": "Lead", "type": "member" }\n },\n "title": "name",\n "records": {}\n },\n {\n "ref": "task",\n "label": "Tasks",\n "singular": "Task",\n "fields": {\n "title": { "label": "Title", "required": true, "type": "text" },\n "project": { "label": "Project", "required": true, "type": "link", "table": "project", "multi": false },\n "assignee": { "label": "Assignee", "type": "member" },\n "status": {\n "label": "Status",\n "required": true,\n "default": ["todo"],\n "type": "select",\n "options": [\n { "alias": "todo", "label": "To do", "color": "slate" },\n { "alias": "done", "label": "Done", "color": "green" }\n ]\n },\n "due": { "label": "Due", "type": "date", "format": "date" },\n "done_on": { "label": "Done on", "type": "date", "format": "date" }\n },\n "title": "title",\n "records": {\n "subtitle": ["assignee"],\n "due": ["due"],\n "task": true,\n "starts": { "assignee": "me" },\n "status": { "field": "status", "closed": ["done"] }\n }\n }\n ]\n}\n```\n\n```json\n{\n "body": {\n "type": "App",\n "key": "projects",\n "name": "Projects",\n "color": "blue",\n "tables": { "project": "<id of the project table>", "task": "<id of the task table>" },\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "project",\n "children": [\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "work",\n "title": "Work",\n "children": [\n "lead",\n {\n "type": "List",\n "entity": "task",\n "columns": ["due"],\n "children": [\n { "type": "Act", "key": "complete", "label": "Complete", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": { "now": true } } },\n { "type": "Act", "key": "reopen", "label": "Reopen", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } },\n { "type": "Act", "key": "complete_all", "label": "Complete", "on": "ticked", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": { "now": true } } }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\nThe tasks as a register of their own, for one person: a one-part Record holds its Fields and Acts directly.\n\n```json\n{\n "body": {\n "type": "App",\n "key": "my_tasks",\n "name": "My tasks",\n "color": "teal",\n "tables": { "project": "<id of the project table>", "task": "<id of the task table>" },\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "task",\n "opens": { "assignee": { "me": true } },\n "children": [\n { "type": "Reading", "key": "open_tasks", "label": "Open", "where": { "status": ["todo"] } },\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n "project",\n "due",\n { "type": "Act", "key": "finish", "label": "Finish", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": { "now": true } } },\n { "type": "Act", "key": "undo", "label": "Undo", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## App rules\n\nEvery rule the check enforces over an app, one sentence each under an id; a finding names its rule\'s id\n(`[list.filter]`), and `reference/<rule id>` is that one rule\'s page. A sentence names the node and the prop an author writes (`List.create`, a `Section`);\neach area stands in the tree where this table says:\n\n| Area | Stands at |\n|---|---|\n| `app` | what every write meets (the tree\'s keys, the names it declares, the tables and rows it carries), the App, and the register List\'s props that belong to the app: `words` (labels), `color`, `scope`, Books, logs |\n| `template` | the document templates an Act makes and an Export fills |\n| `list` | a List: its columns, filters, search, group, sort, layout, tabs, period, export, and what a child List holds |\n| `reading` | a Reading or Progress |\n| `record` | a Record and its parts: Section, Side, Page, Act placement |\n| `statement` | a Statement |\n| `act` | an Act |\n| `check` | a Check |\n\n<!-- generated:start rules-app -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.schema` | Every key is one its table lists, holding the type its row gives, and every required key is stated. |\n| `app.names-declared` | Every alias a key names is declared: a table of the App\'s `tables`, a field of the table the node reads, an option of the select it names, an Act `key` of the App, a template of its `templates`, or an app. |\n| `app.tables` | A call declares at least one table and at most 50; send the rest in a second call. |\n| `app.rows-cap` | Rows are a sample: at most 200 per entity, 2000 per call and 2000 documents attached \u2014 a real data set goes in with `create_records` once the tables exist. |\n| `app.alias-scope` | An alias is unique where it is named: tables, fields and views in their table, options in their select. |\n| `app.label-unique` | A label is unique where it is found by its label: fields and views in their table, options in their select \u2014 and no view takes its table\'s own label, which names the whole-table grid. |\n| `app.filter` | A filter \u2014 a view\'s, a rollup\'s \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares. |\n| `app.alias-unique` | An App\'s `key` is unique among apps, and an Act\'s among the Acts of its App, every List\'s included. |\n| `app.icon` | An App\'s `icon` is a Lucide icon name in kebab-case, as `receipt` or `trending-up` are. |\n| `app.add-label` | A List\'s `add_label` stands only where it adds rows (`create`), in words that are not empty and hold no `{` or `}`. |\n| `app.labels` | An App\'s `words` name a table it reads and a field or option of it, in words that are not empty and hold no `{` or `}`; no two fields of a table, nor two options of a field, show the same words. |\n| `app.records-entry` | Every table an App draws a List over, opens or picks from \u2014 the register\'s, a child List\'s, a linked row\'s, a documents List\'s \u2014 has a `records` entry. |\n| `app.scope` | The App\'s `scope` is a required one-row link of the first List\'s entity, the same on every row the App lists \u2014 never a `columns`, `filters` or `search` entry, `group`, `where`, `opens`, a Field of a part or a `create` question. |\n| `app.when` | A node\'s `when` holds single selects to options, yes/nos to `true` or `false` (stored or a formula of one; a formula with `options` is a select), numbers and dates to bounds (`gt`, `gte`, `lt`, `lte`; a date\'s bound is a day, a moment on a datetime, or `{ "now": true }`) and any field but a yes/no to `{ "empty": true \\| false }` \u2014 a number read in each row\'s own unit or currency and a lookup take no bound. |\n| `app.asked-written` | What a List\'s `create` asks, what an Act\'s `asks` asks and what `starts` fills is a field a person writes \u2014 never a formula, rollup, lookup, autonumber or a date the workspace stamps. |\n| `app.own-rows` | A many-link to rows that each belong to one row of this entity is never a Field of a part, a `create` question or an Act\'s `asks`: those rows are the record\'s own, read as a child List. |\n| `app.create-asks` | A List\'s `create` asks fields a person writes \u2014 never the link or the folder it files the row under, which the add sets itself. |\n| `app.create-required` | A List\'s `create` asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add. |\n| `app.books` | A register App\'s Books each name another entity once (`entity`) \u2014 never the App\'s own \u2014 whose fields line up with the register List\'s by alias, else by `fields` (a field the List reads, or one an Export prints \u2192 one of the Book\'s): read as one column type (a day apart from a moment), one value or several alike, a link to the same entity. Each Book holds the title, the status where the List shows one, the folder (`scope`), what `where` keeps and every Check locking the rows; the List is a table, cards or a plan, whose `at`, `across` and `up` line up like any field. Two Books holding a field of their own under one alias hold it as one kind of value. An Export\'s `books` each name the List\'s entity or one of its Books, once, and one of them holds each field its `where` and `per` narrow by. A Book\'s `column: true` draws the column naming each row\'s table in a List that has Books. A Book\'s own Acts and Checks, over its rows in its own fields, stand beside the List\'s: each Act\'s `key` its own among the App\'s Acts, its Books\' and its logs\', and its `workflow` run by no Act of another List or Book. |\n| `app.book-option-kin` | *Noted, never refused:* A Book\'s option labelled as one of the App\'s, under another alias, reads apart from it: the List lists both and counts each on its own. |\n| `app.book-lacks` | *Noted, never refused:* A Book lacking a field the Record, an Act or a `create` reads: what reads it drops from the Book\'s rows \u2014 an Act, a child List, a Section, the `create`. An Act of the List running its own `workflow`, reading papers or recording a call, and an Act of a child the Book\'s record lists no rows of: none is offered on the Book\'s rows, which run the Book\'s own Acts. A field only an Export prints, held by a Book as another kind of value, prints blank on its rows. A Tab\'s `totals` over a child linking to no row of a Book total nothing on its rows. |\n| `app.logs` | A register App\'s logs \u2014 further Lists, at most 3 \u2014 each list another entity under a `key` of their own: never the App\'s own entity, one of its Books\' or another log\'s. A log is one List \u2014 a table or cards, with no Tabs, no Book `column` and no composed Record \u2014 over an entity whose status states no owned `stages`, and stands in an App that states no `scope`. An Act\'s `key` is its own among the App\'s Acts, its logs\' included. No Record of the App or of a log lists the rows of an entity another of the App\'s Lists is over: a link to one opens it on its own List. A List people work as a desk of its own is another App. |\n| `app.row` | A List\'s `row` states only what differs from `records`: a `subtitle` that is not the title, a number for its `figure`, dates for its `due`, an `image` that is a files picture no Act makes documents into, or `false` for a figure, deadlines or picture the entity states and the App draws none of. |\n| `app.tasks` | *Noted, never refused:* A task entity an App lists whose rows no write of the App moves to a closed option (its ring stands disabled), or that an Act ticks done and none moves back. |\n| `app.workflow-value` | A value a Workflow takes or hands back states what its type needs and nothing another type does: a `record_link` its `entity`, a `select` its `options` or the `field` holding them \u2014 never both \u2014 an `object` its `fields`, an `array` its `items`; `multi` stands on a `record_link`, `select`, `member` or `file`; an output is never a `member` or a `file`; and a value nests at most 8 deep. |\n| `app.query` | A Query reads one entity\'s rows as `fields` or as a `reading`, one of the two \u2014 never the App\'s own entity where the App has Books, whose List reads several tables; `sort` and `limit` stand with `fields`; a `reading` splits by `by` or by the day of `over`, never both, and averages only with neither; each param is a number, a yes/no, a date, a select, a one-row link or a member holding one, read by an entry of `where` on a field of its type \u2014 a date by a range, a number by either, and never a number read in each row\'s own unit or currency or a date holding a span. |\n| `app.generated-name` | A name the App generates is taken by nothing the App states. No Query, no Reading\'s or Progress\'s `key` \u2014 a log\'s as it is read, `<entity>_<key>` \u2014 and no Tab\'s read (`<entity>_list_<tab>`) is the name of a read the App generates, of a Query or of a Reading. |\n| `app.surface-described` | *Noted, never refused:* A value a Workflow takes, and a param a Query takes, that states no `description`: the one line a caller reads to fill it. |\n| `app.keys-unique` | No two children of one node share a key: an address names one node. |\n| `app.states-list` | A register App states the List whose rows it lists; an App stating none is a dashboard and holds Readings. |\n| `app.period` | An App\'s `period` is a dashboard\'s: a register states its own on its List. |\n| `app.dashboard-words` | A dashboard reads every table\'s rows whole and states no `words` or `scope`: those are a register App\'s. |\n| `app.add-once` | A table\'s add by a picker is stated once \u2014 at one Field\'s `add` or one link input\'s `add` \u2014 and every picker of that table adds by it. |\n| `app.card-once` | A table\'s card (`record`, `remove` on a link Field) is stated once, at one Field. |\n| `app.card-fields` | A card\'s `record` names, once each, fields of the linked table its save may change. |\n| `app.picker-link` | `add`, `record` and `remove` stand on a Field that is a link: only a link has a picker and a card. |\n| `app.add-filed` | The Lists adding rows of one table file them by one link, the first such List\'s: a List over that table filed by another link lists its rows and adds none. |\n| `app.default-breaks` | A node the platform draws where the tree states none leaves the App refusing nothing more: where it would, the tree states that node itself. |\n| `app.adds-field` | A picker\'s `add` names fields of its table a person writes, each once. |\n| `app.picks-only-field` | `add: false` stands at a link, once, to an entity whose add the app states. |\n| `app.edits` | *Noted by the designer on a write, never refused:* The fields an app\'s saves may change are the fields its surfaces write \u2014 a record\'s save, a child row\'s, an in-place column, a lane, gantt or plan move \u2014 less what its own Acts change; the designer notes an app whose saves change none. |\n\n<!-- generated:end rules-app -->\n\n## Reading rules\n\n<!-- generated:start rules-reading -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.value` | A Reading sums a number, never a percent \u2014 percents do not add up; a value of `{ avg }` reads one. |\n| `reading.average` | `{ avg }` averages a Reading\'s `value` over the rows holding one, read against a plain `target` \u2014 never a value `limits` bounds, whose bound is a sum. |\n| `reading.split` | `by` and `across` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 or by the bands `tiers` cut a number read in one unit on every row; a date `by` splits them per period, as a trend; a grid crosses two different fields. |\n| `reading.over` | `over` is a date: the row\'s own, stored or a formula, or its parent\'s through a lookup over a one-row link. |\n| `reading.where` | A reading\'s and a Query\'s `where` keep rows by a select\'s options, a yes/no\'s `true` or `false`, a number or date\'s bounds (`gt`, `gte`, `lt`, `lte`; a date\'s `{ "now": true }`), `{ "empty": true \\| false }`, or a member field holding the reader (`{ "me": true }`); a List\'s `where` also keeps rows by `<link>.<field>`, a select or yes/no of the row a one-row link names; a Query\'s `where` also holds a field to what the call states (`param`, `from`, `to`). |\n| `reading.target` | `target` reads a value nothing bounds \u2014 a value `limits` bounds reads against the sum of its bound. |\n| `reading.better` | `better` needs `over`: a change is read over the periods its date places the rows in. |\n| `reading.list-app` | A List\'s `app` is the id of an app whose first List is over the List\'s own entity. |\n| `reading.progress` | A Progress reads dates of its rows for `planned` and `finished`, a number for `done` and `weight`, and rows `measured` through a one-row link to them, by a date of theirs and a number of theirs. |\n| `reading.alias` | A Reading\'s or Progress\'s `key` is its own among the App\'s Readings and Queries, a log\'s read by its entity before it (`<entity>_<key>`); a Progress above a List reads the List\'s own rows and states no name of its own: its `key` is the one its place gives it, `register_reading_<position>`. |\n| `reading.tabs` | A dashboard with Tabs holds every Reading and List inside a Tab, and every Tab holds one at least; without Tabs, none sits in one. |\n| `reading.restate` | A dashboard\'s count tile never counts the rows a List beside it lists under the same `where` and tab: the List\'s head counts them. |\n| `reading.period` | A dashboard\'s `period` is the one it opens on, and it stands only over a Reading placed in time by `over`. |\n| `reading.funnel-place` | A Reading with `steps` (a funnel) stands in a part of a Record: a band and a dashboard draw none. |\n| `reading.on-hand-split` | A Reading with `on_hand` compares what is owed with what is on hand per value of one field, or two: its `by` and `across` name fields, never bands. |\n| `reading.average-single` | An average (`value: { avg }`) is a single figure\'s: its Reading states no `on_hand`, `by` or `across`. |\n| `reading.grid-rows` | `across` splits a grid\'s columns, and stands only beside the `by` splitting its rows. |\n| `reading.tab-holds` | A register\'s Tab narrows its rows and holds no nodes: Readings stand beside it in the List. |\n| `reading.band-off` | `readings: false` turns off the band a register leads with by default: a List stating it states no Readings or Progress, which are the band. |\n\n<!-- generated:end rules-reading -->\n\n## Statement rules\n\n<!-- generated:start rules-statement -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `statement.lines` | A Statement\'s `lines` are numbers of the record, each once \u2014 the first stating no `op`, every other one, the last an `equals` \u2014 each `equals` a formula, rollup or lookup, and a `rate` a number of the record. |\n| `statement.rows` | A line\'s `parts` read rows of an entity holding `same`, a one-link of the record\'s to the same entity; `at`, a moment of those rows, and `before`, a moment of the record (datetimes, never days), stated together; their `columns` and `figure` are those rows\' fields, the figure a number. |\n| `statement.parts-keyed` | A line\'s `parts` add up to its figure, so no person keys it: a formula, rollup or lookup, a value the server copies when the record is made (`default_from`) that no `create` asks and no save or Act writes, or a stored number the App\'s saves leave out \u2014 the line draws it read; each part\'s `figure` is frozen when its row was certified \u2014 a stored number an Act fills, the server copies or the App\'s saves leave out, never one a person keys or the server recomputes \u2014 and no person keys the moments `at` and `before` either. |\n\n<!-- generated:end rules-statement -->\n\n## Check rules\n\n<!-- generated:start rules-check -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `check.field` | A Check\'s `field` is a yes/no or text formula of the rows of the List it stands in. |\n| `check.of` | A Check stands in the register List or in a child List the Record lists. |\n| `check.not-placed` | A Check\'s field is never also placed on the record as a Field \u2014 the line under the head says it. |\n| `check.blocks` | A Check\'s `on` names what it refuses: `"update"` (the saves), an Act\'s `key`, or `"all"` (every Act and save); a child List\'s Check refuses that row\'s saves and the Acts of it, never the record\'s. It binds this App alone. `"delete"` refuses this App\'s removal of the rows and is kept for a rule no check of the table states: a Delete is the table\'s checks\' to refuse. |\n| `check.resolve` | A Check\'s `resolve` names an Act of this App on the rows the Check stands on, pressed on one row, not refused by the Check, and drawn as a button \u2014 never a correction. |\n| `check.restates-meter` | A warning reading a gated meter\'s figure against its bound or pass mark is refused \u2014 the meter draws it. |\n\n<!-- generated:end rules-check -->\n\n## List rules\n\nThe rules of a List\'s props \u2014 columns, filters, search, group, sort, layout, tabs, period and export \u2014 and of what a child List holds: a Field gallery or prose block, a Reading, a desk, a timeline or an agenda. A layout\'s, a Tab\'s, an Export\'s and a desk\'s rules stand with their keys.\n\n<!-- generated:start rules-list -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `list.column-drawn` | A List\'s `columns` never name what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 and name a field once; a child List\'s never name its link back either, and an `expect` List\'s figure is its filled-in-place column. |\n| `list.columns-budget` | A List\'s `columns`, each at the width of its kind, fit the room a desk leaves beside its name and the row\'s own status, deadline and figure. |\n| `list.filter` | A List\'s `filters` name a select, a member, a link, a date, a yes/no or a number of the List\'s own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes. |\n| `list.filter-split` | *Noted, never refused:* A `filters` entry on a field a Reading of the List with `by` or `across` splits the rows by \u2014 a press on its part narrows by it too; a member\'s filter, which narrows to the reader\'s own rows, is never noted. |\n| `list.filter-unshown` | A `filters` entry, a narrowing the List `opens` on, and a press in its band (a Reading\'s part, a grid\'s cell, a figure\'s `where`) name a field its rows show \u2014 a `columns` entry; the row\'s title, the line under it, its picture, status or figure, a meter\'s bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under \u2014 or a formula, drawn where every field of its own row it reads is (through other formulas too; one reading no field is drawn nowhere). A narrowing to the reader (`{ "me": true }`) needs none. A tree leaves out a `filters` entry no row shows and says so (name the field as a column or in the row\'s line to offer it); the other two are refused. |\n| `list.tiers` | `tiers` band a number read in one unit on every row; a figure read in each row\'s own unit or currency is named bare. |\n| `list.search` | A List\'s `search` names text, a code, or a link \u2014 matched by its row\'s title. |\n| `list.sort` | Each `sort` key of a List or a Tab names a field of the List\'s rows that holds an order \u2014 never files \u2014 and no field twice. |\n| `list.group` | A List\'s `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 the status too, but never on a task list\'s block, whose chips narrow it; only a table or cards List groups, and a gantt into its lanes; grouping by the title needs a `row.subtitle` to lead each row. |\n| `list.readings-own` | A Reading in a List reads the List\'s own entity; another entity\'s stands on a dashboard. |\n| `list.readings-bands` | A Reading in a List that bands a number by `tiers` has that field with the same tiers among the List\'s `filters`: a press on a band narrows the rows by that filter. |\n| `list.readings-restate` | A Reading with no `by` in a List never sums the row\'s figure, counts the rows, or counts one stage\'s rows, unless it is placed in time (`over`): the summary line totals the figure and the status chips count the rows, but neither draws a change against the last period. |\n| `list.where-opens` | A List\'s `where` holds its own selects, yes/nos, bounds and blanks (a child List\'s or the register\'s), and `opens` its selects and yes/nos and a member field holding the reader (`{ "me": true }`) \u2014 or, on a status whose `stages` have owners, the rows waiting on the reader; a field `where` fixes to options is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. A List laid out as a `plan` keeps its items by options and yes/nos alone. |\n| `list.roster` | A List with `layout: "roster"` holds the List of its cells: the cells\' entity has one single link to this one and a date on its line (a field of the row itself, or a lookup of its parent\'s date), a roster draws no `columns`, and an `expect` (with `of`) stands only on the cells List. |\n| `list.roster-expect` | A roster cells List\'s `expect` is a single select of its rows, and its `of` this entity\'s multi-select whose every option is one of `expect`\'s. |\n| `list.band-pivot` | *Noted, never refused:* A Reading with `across` (a grid) among a List\'s Readings: its grid reads on a dashboard, and the band leads with a figure (a Reading with no `by`). |\n| `list.headline` | *Noted by the designer on a write, never refused:* A List over rows that fall due or carry money and have no status whose steps the chips count, laid out as a table, cards or a gantt, has Readings \u2014 they split the rows by a field they show. |\n| `list.span` | *Noted, never refused:* Rows that each span a run of days or hours \u2014 two dates on their line, a `frees`, a `no_overlap` booking them on what its `by` links to, or one required date and the table\'s `due` where the line names no date \u2014 are drawn in time by some List over them (a `layout` of calendar, lanes, gantt or roster) \u2014 the note names the app to add over them, laid out as lanes on what each one books, else as a gantt (a run held by a date and the `due` also asks for both dates on the line). |\n| `list.where-bound` | A bound read from the record (`{ "record": \u2026 }`) in `where` narrows only a List that reads its rows \u2014 `same`, `app`, or a Record with `edit: false`. |\n| `list.where-reader` | A List\'s `where` keeps its rows for every reader and never names the reader (`{ "me": true }`): `opens` opens a List on the reader. |\n| `list.props` | A node states only the props of its place: a List those of the register, a log, a roster\'s cells, a rows, agenda, timeline or read-only List of a record, a document or a dashboard; the Record of a List below the register `edit` and its children; a dashboard\'s Tab its `key`, `label` and children. |\n| `list.holds` | A List holds what its place draws: the register its Tabs, Readings, Progress, Exports, Acts, Checks, Books, Record and, under a roster, the one List of its cells; a child List its Acts, Checks, Record and, with `under`, the List of its documents; a document and a roster\'s cells their Acts, Checks and Record. |\n| `list.layout-place` | The register lays out as a table, cards, a calendar, a roster, a gantt, lanes or a plan; a child List writing its rows as a table, a checklist or a desk; any child List as an agenda or a timeline. |\n| `list.row-restates` | A child List and a log read their rows as `records` states them: their `row` states a picker\'s add or a card on the title or a subtitle, never another `subtitle`, a `figure`, `due`, `image` or `status: false`. |\n| `list.edit-column` | An edit column (`{ "field": \u2026, "edit": true }`) stands in a child List, which writes its rows in place: the register\'s rows are written where they open. |\n| `list.child-link` | A child List reads a child linked to the record; `via` names the link where the child links more than once. |\n| `list.row-blocks` | The Record of a List below the register \u2014 a rows List\'s, a document\'s under its lines, a roster\'s cells\' \u2014 holds its Fields and the Lists of its rows\' own children, each a child List as one level up over an entity linking to that List\'s, checked against that List\'s entity; nothing else. A List below a row states no `under`, `pick`, `expect` or desk, and a List stating `expect`, `under` or a desk holds no List below its rows. |\n| `list.title-back` | Where a child\'s title is its link back to the record a child List lists it under, its first other `row.subtitle` names it there \u2014 a text, a code, a select, a link or a member, never a number or a date. |\n| `list.where` | A child List\'s `where` names single selects the child requires, by options they hold \u2014 one held at a single option is never also a column \u2014 and the child\'s yes/nos by value, numbers and dates by bounds, and fields by `{ "empty": true \\| false }`. A yes/no, a bound or a blank is never held: a child List adds rows only at a stored yes/no\'s start, adds none under a bound or a blank, and never picks, expects or stands lines under documents. |\n| `list.under` | `under` names the child\'s one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines\' columns never read the document (its heading does) nor hold long text. |\n| `list.under-once` | Documents drawn over their lines (`under`) are never listed again as a List of their own. |\n| `list.edits` | A child List\'s editable columns (`{ "field", "edit": true }`) name at most two of the child\'s columns or its figure that the App\'s saves of the child write \u2014 a number, a date, a few words, one option or a one-row link, never a files or long-text field, the List\'s `via` nor a link it must name alike (a link\'s `same`) \u2014 never on an `expect` List. |\n| `list.remove-roster` | A roster cells List\'s `remove: true` offers the Delete of the roster\'s rows, so it stands only beside a roster over rows the App writes. |\n| `list.attention` | `attention: false` keeps a table\'s or cards\' rows in the List\'s own order \u2014 a first-come queue; a calendar, lanes, a gantt and a roster stand their rows in time and lift none. |\n| `list.remove-documents` | The documents List inside a child List with `under` states `remove: true` only over documents the App writes. |\n| `list.compare` | A child List\'s `compare` names two different fields of the child, `before` and `after`: two files fields, or two values of one type (text but long text, a number, a date, a single select). Never with `expect`. Over files on a checklist layout, `before` is the check\'s `photo`. Over values: neither side is a column or something the row draws already (title, line, figure, status, deadline), two numbers share a notation, no column is long text, never on a checklist layout. |\n| `list.current` | A child List\'s `current` names a date or number of the child (stored, or a formula yielding one) that orders a set of revisions: the row holding the latest value is current, the rest fold as superseded. Never with `group`, `under`, `expect`, `checklist`, `compare`, `post` or `pick`, nor beside a column of long text. |\n| `list.pick` | A child List\'s `pick` links rows of the child through `via` \u2014 an optional one-row link a person writes, no Act writes, no other rule compares rows by and no `under` List stands lines under \u2014 paired with a many-link of the record its own save writes; the child\'s `read_scope` reads a row naming no record through `via`; never with `expect` or `under`, nor where the App\'s saves leave out the child or the many-link. |\n| `list.narrow` | A child List\'s `filters`, `search`, `group` and `sort` follow the register List\'s rules over the child\'s rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, nor on an `expect` List; an `under` List is grouped by its documents. |\n| `list.expect` | `expect` names the child\'s title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the List; its `columns` name a field filled in place \u2014 unless the List\'s layout is a desk, whose lines are ticked \u2014 and its `create` takes the file where the child is pictured by one. |\n| `list.expect-fills` | An expected line is made from its key and its first filled column \u2014 a desk\'s from its key alone, by its tick \u2014 so every other field the child requires has a default, a start or a `default_from`, or is read first. |\n| `list.expect-of` | An `expect` object\'s `of` or `only` needs a select: `of` names the record\'s multi-select of the title\'s options; `only` names options of a single select, never with `of` or a `where` on it, one List per option; an Act\'s `paper` is drawn in one. |\n| `list.funnel` | A Reading\'s `steps` (a funnel) are 2 to 8 numbers of its child, each named once \u2014 stored, a formula or a rollup \u2014 all in one notation, never a percent nor a unit read on each row. |\n| `list.list` | A read-only child List (`same`, `app`, or a Record that is `edit: false`) lists the rows of an entity that link to the record by its one link to it (`via` where it has several); with `same`, a one-row link of the record, it lists the rows sharing that link\'s row with the record \u2014 the record left out \u2014 by the listed entity\'s one link to that row\'s entity (`via` where it has several). `same` stands in a Record, never on a dashboard. Its Record\'s Fields \u2014 at least one, each a field of the listed entity named once, never its title nor one of the `columns` \u2014 are what a row shows, read-only, when it is opened in place. |\n| `list.list-opens` | A read-only child List\'s row opens in the record\'s drawer as a record: its entity is one a List of the App draws \u2014 the register\'s, a Book\'s, or a log\'s \u2014 or the List\'s `app` names the register app that works it. |\n| `list.figures` | A Record\'s own figures (Readings with no `entity` and no `by`, whose `value` names the field) are 2 to 8 numbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number the App\'s saves leave out \u2014 each placed once on the record. |\n| `list.create-boolean` | A document\'s and a roster\'s cells\' `create` is `true` or `false`: their add asks the table\'s defaults. |\n| `list.via-path` | A `<link>.<link>` `via` reaches rows through a row of their own and only reads them: a List writing its rows lists a child linked to the row it stands under. |\n| `list.read-label` | A List that only reads its rows \u2014 a read-only child List, an Outside\'s List, a dashboard\'s List \u2014 states its `label`. |\n| `list.create-empty` | A child List that adds rows asks the fields of its add: where the table\'s defaults ask none, state `create: false`, or the fields it asks. |\n| `list.read-scope` | A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history\'s, which takes its record\'s. |\n| `list.unworked-stage` | *Noted, never refused:* A register lists rows (every open one, else those its `where` keeps) at a stage that no Act of the App is offered at and no Section stands at, though the App states the stages it works: the rows only sit in its list, so keep them out with `where` or leave them to the App that works them. |\n| `list.under-note` | *Noted, never refused:* Rows holding a paper that cover another List\'s lines, listed apart with no add of their own stated: `under` draws them over the lines they cover. |\n| `list.expect-note` | *Noted, never refused:* Rows each a kind of paper, listed with no `expect` and no `where` on the kind, that no Act files a call into or `fills`: the lines still missing read as what a record needs. |\n\n<!-- generated:end rules-list -->\n\n## Record rules\n\n<!-- generated:start rules-record -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.placed-once` | A Field, a child List, a Reading, a Statement and an Act each stand once in a Record: its head draws the title, subtitle, image, status, figure and folder, and a field\'s one other place is a part\'s Fields, a gallery or prose Field (`block`) or a Statement \u2014 save the image, which one files gallery may draw again, whole, the figure, which a Statement draws as what it comes to, and rows one List prices and another compares (`compare`); two places whose `when`s never hold together (a select at options the other\'s lacks) each draw it, and a plain child List (no `expect`, `under`, `pick`, `compare`, nor a `layout` of desk or checklist) likewise. |\n| `record.foot-total` | A child List\'s foot totals the child\'s figure and each summed column over the rows it lists; the record\'s rollup summing that field over those rows (the same `where`) is that foot, never also a Field of a part. |\n| `record.fact-restates` | A Field of a part never looks up what its link already reads where it stands: the linked row\'s title beside the link, the figure or bound its meter reads on the head\'s line. |\n| `record.section-holds` | A part (Section, Side, Page) holds Fields, child Lists, Readings, Progress, Statements or Acts. |\n| `record.compose` | A Record\'s `compose` is stated instead of parts (it holds no children), on a page `door`, by an App with no Books whose records have no owned stages (`status.stages`) and no picture (`image`) \u2014 a composed page draws neither. Each name is a field of the entity, an Act of the app on the record or a child entity linked to it; one `Title`, and each field, Act and child drawn once \u2014 a field by `Field` or `Document`, never the title or the status, which `Title` and `Status` draw. The first `Act` the page itself draws is its next step and every other one secondary; an `Act` is a button alone, never one making a set of papers. A `Bar` holds no `Rows` and no `Document`, and an `Act` in it that destroys or corrects what is done (`danger`, or a move back) stands in the record\'s \u22EF instead (the Bar piece\'s own row says how it draws). A `Panel` draws nothing in the page: it is one entry of the record\'s \u22EF, named by its `title`, opening its pieces in a panel beside the page; it stands directly in the root `Column`, holds `Rows`, `Field`, `Act`, `Group` and `Document` \u2014 never `Title`, `Status`, `Bar`, `Copy` or another `Panel` \u2014 and an `Act` directly in it ends the panel\'s name, secondary. A `Group` is headed once, by its `title`: holding one `Rows`, that heading carries the rows\' count and their add in place of the rows\' own, and the `Act`s the group holds end its line. A `Copy` takes a text field\'s words as the page holds them now, a `Document`\'s words not yet saved among them; a field a `Copy` alone names is read, never written. |\n| `record.parts` | *Noted by the designer on a write, never refused:* A List laid out as a table, cards or a gantt opens a Record whose Sections hold the fields people write, a files Field per paper, a child List per child and the record\'s Acts. |\n| `record.header-budget` | *Noted by the designer on a write, never refused:* A Record\'s head says what the row is and where it stands \u2014 its title, line, picture and status \u2014 and at most two more facts, one of them a deadline at most; the rest stands in a Section. |\n| `record.lists` | *Noted by the designer on a write, never refused:* A table some App records, whose rows other rows each belong to (a one-row link to it, never its status history), draws some of them under its row \u2014 a child List in its Record, a List with `layout` roster or lanes by that link, or an App over them working inside one of its rows at a time (`scope`). |\n| `record.at` | A Section\'s `at` names options of the record\'s status; a record with no status has no steps. |\n| `record.at-once` | A stage is the work of one Section: two Sections naming one option of the status in `at` hold some Field to values apart in their `when` \u2014 a `when` holding the status keeps its Section off the options it leaves out. |\n| `record.section-page` | A Page stands only on a page `door` \u2014 never a drawer\'s or one beside the List \u2014 is never a step (a Page has no `at`), has a `title` naming an address no other Page shares (a letter or a digit at least), and is never every part. |\n| `record.side` | At most one Side stands in a Record, on a page `door`, and it is never also a Page. |\n| `record.beside` | A Record with `door: "beside"` opens beside a table List \u2014 never a board, cards or a calendar, which draw no list to stand beside. |\n| `record.history` | No part of a Record reads the entity\'s status history (`status.history`): the rail reads each move. |\n| `record.section-acts` | An Act in a part works on the record itself; an Act in a child List stands on the child\'s row, and an Act in the register List with an `on` on the register\'s rows. |\n| `record.act-in-section` | Every Act of the record stands in the part whose work it is; only a correction no part owns \u2014 `danger`, and no other status move offered at the same moment \u2014 or an Act an agent runs for the person (a `workflow` or an `intake` moving no status), placed in the List and in no part, waits in the head\'s \u22EF. |\n| `record.move-home` | A status move offered at one step stands at that step\'s foot when it moves the work forward, to an exit, or back to a step it passes anyway; one offered at several steps, or moving back into a detour, stands in the Section whose `at` holds the status it enters. A tree\'s status move written in a Section with no `at`, or in none, is placed where this puts it and said. |\n| `record.act-staged` | An Act at a staged Section\'s foot is offered only during that Section\'s steps: its `when` holds the status to options the Section\'s `at` names. A tree\'s Act with no `when` at the foot of a Section staged at one step is offered at that step and said; a correction stays offered at every step, and an exit is refused. |\n| `record.act-hidden` | An Act is never offered only while its part\'s `when` hides the part. |\n| `record.outside-place` | An Outside stands directly in a Record whose table states a stored status select, and its `key` is unique in the App: a link to a record names it. |\n| `record.outside-at` | An Outside\'s `at` names options of the record\'s status that no stage owns, and no two Outsides of a record open during the same option. |\n| `record.outside-field` | An Outside\'s Fields are read as values, each a field of the record shown once, never `edit`, `when` or a label of its own. |\n| `record.outside-list` | An Outside\'s Lists read the child rows under the record as they are: their `columns`, `label` and `where` only, never an add, a Delete, a pick, a filter, a search, an `app`, a link that opens, or a bound read from the record. |\n| `record.outside-act` | An Outside\'s Acts are its alone and each ends the visit: `when` holds the status to `at`, `set` moves it to an option outside `at`, `asks` are text, numbers, dates or listed options, `requires` names fields the Outside shows or the act asks, a `sign` is by a text field with no witness, and none states `on`, `of`, `workflow`, `templates`, `paper`, `intake`, `record`, `review`, `open`, `danger`, `recommends` or `me`. |\n| `record.field-edit` | An Act that only writes one field of its record, not the status, from the one input it asks is that field\'s edit: the Field rests in the part as a control. |\n| `record.holds` | A Record holds parts (Section, Side, Page) or the Fields of its one part, never both; the Record of a Book, or of a child List no row opens in place (an agenda, a timeline, a read-only List), holds Fields alone. |\n| `record.field-heading` | A Field drawn as a control states no `label` or `when`: only a gallery or prose Field (`block`) has a heading and a condition. |\n| `record.section-owners` | *Noted, never refused:* A Section holding acts offered at stages that different owners work \u2014 a role apiece, or a member field apiece \u2014 is two jobs in one: each owner\'s part is a Section of its own. |\n\n<!-- generated:end rules-record -->\n\n## Act rules\n\n<!-- generated:start rules-act -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.paper-note` | *Noted, never refused:* A made paper kept in a field of a record that lists its papers by kind: it is one of those lines. |\n| `act.moves` | *Noted by the designer on a write, never refused:* Rows a stored status stages towards `closed` options are moved by an Act of some App over them, or by its authored `workflow` \u2014 each open option moves to the next, the last to the first closed one. |\n| `act.set-not-asked` | A field an Act `set`s (an option, a value, `{ "now": true }`, `{ "me": true }`, an input) or `sign`s is the Act\'s outcome: the App\'s `create` never asks it, and a required one takes a `default`. A `formula` only starts a value, which a person may change after. |\n| `act.asks-set` | A field an Act `asks`, `set`s, `sign`s or `writes` is stated by one of them: what the Act writes is its outcome, never also asked, and no field is written by two clauses of one Act. |\n| `act.writes` | `writes` stands beside `workflow` and names stored fields of the acted row, each once, that no `set` (its own or another Act\'s) states. |\n| `act.starts` | `starts` (an ask `{ field, starts }`) starts a field the Act asks \u2014 `today` a date or a moment (never a span), `me` a member \u2014 for the reader pressing it, who changes it at will. |\n| `act.set-formula` | `set: { <field>: { formula } }` reads fields of the acted row and computes what the field holds \u2014 a number, a date, text, a yes/no or a single select\'s option (`{field:option}`), typed as the entity\'s own formulas are \u2014 never the status; a bound a write rule holds the field to is checked on the computed value as the Act runs. |\n| `act.of` | An Act in a child List works on that child: the one row pressed \u2014 or, with `on: "ticked"`, the rows ticked together where the record lists them as tasks \u2014 never the rows in view. |\n| `act.files-written` | An Act\'s `writes` names no files field: a file an Act makes is a paper, filed in a slot it names with `paper` (a child List with `expect`). |\n| `act.writes-line-key` | An Act never writes the link or the key naming an expected line under its record \u2014 a line\'s key is stated by filling it. |\n| `act.makes-or-runs` | An Act makes a document (`template`, `templates`) or runs an authored `workflow`, never both \u2014 the workflow makes what it needs. |\n| `act.does-nothing` | An Act does something: it `set`s a value, `asks` a field, runs a `workflow`, makes a document, or takes an `intake`, a `record` or a `sign`, or `open`s an app. |\n| `act.view` | An Act over every row in view (`on: "view"`) writes (`set`), makes a document or runs a workflow, and states no `requires`. |\n| `act.requires-drawn` | Every field an Act `requires` is drawn where the Act stands \u2014 its part\'s Fields, a child List\'s, its `asks`, or an unstaged part\'s where its own is unstaged \u2014 never the head alone. |\n| `act.requires-written` | A field an Act requires that only an Act writes is written by an Act offered together with it, or asked by the Act itself. |\n| `act.recommends` | A field an Act `recommends` is not also in its `requires`. |\n| `act.set` | `set` writes each field a value it holds \u2014 an option alias, a number, a yes/no, a text, `{ "now": true }` on a date, `{ "me": true }` on a member, `{ "input": "<name>" }` of an input the Act asks of the same kind \u2014 or clears an optional one with `null`. |\n| `act.into` | `into` is a files field of the one record the Act is pressed on, beside its `template` or `templates`. |\n| `act.paper-list` | A List a made paper names is one it can list: none keeps its rows by the reader, and no name is reached by a child List that writes its rows and one that only reads them, or by two of either, at once. |\n| `act.paper` | `paper` is a kind the record lists its papers by, on one record \u2014 never beside `into` or `templates`; one maker per kind. |\n| `act.templates` | `templates` names each template once, kept in `into`, on one record pressed alone \u2014 never beside `template` or `asks`, in a child List, or with an `on` over several rows. |\n| `act.intake` | `intake` names a child the record lists in a child List with `expect` on its title \u2014 a single select, the paper\'s kind \u2014 and is narrowed by no `where`, holding exactly one files field and needing nothing an added line does not fill; the Act runs on one record, states `fills`, and states nothing but its `label`, `when` and `requires` beside them. |\n| `act.record` | `record` names a child the record draws in a timeline List or a child List with no `expect`, `under` or `where`, linking back by one single link, its row needing nothing the recording does not write: `audio` and `video` files fields, `transcript` text, `at` a date, `by` a member, each its own field; on one record, stating only `label`, `when`, `requires` and `fills` beside it. |\n| `act.sign` | `sign` runs on one record, never a child\'s row: `into` a files field, `by` a text field the signer types or a member field set to the person pressing, `at` a date, `witness` a member field \u2014 stated exactly beside a text `by` \u2014 each a field of its own that the Act does not also ask or set, and no other Act signs into `into`. |\n| `act.open` | `open` opens another app at the record and does nothing else: an Act on one record of the app\'s own, stating only `label` and `when` beside it. |\n| `act.open-app` | An Act\'s `open` is the id of an app of this workspace that reaches the record\'s table: a register over it, or a custom-code app declaring no table or declaring it. |\n| `act.fills` | `fills` names each once, only beside `intake` or `record`. Beside `record`: a field of the recording\'s row the recording does not write, never a link. Beside `intake`: a field of the record, `<link>.<field>` on the row a required one-row link names (never beside that link), or a child the record adds rows of in a child List with no `expect`, `under` or `where`, filled in the fields its `create` asks \u2014 every one a new row needs among them. Each field filled is one a person writes \u2014 text, a number, a date, a yes/no, options or a single link\'s row \u2014 never a status, a field an Act writes or a link `same` narrows; a link\'s row is one the App reads of its entity. |\n| `act.fills-drawn` | *Noted, never refused:* A field `fills` writes that the record draws nowhere: the review shows the change, the record does not. |\n| `act.fills-new` | *Noted, never refused:* A child `fills` adds rows of, whose natural key the papers cannot state: every row read is added, so the same papers read twice list them twice. |\n| `act.input` | An input has a name of its own \u2014 never another ask\'s, a field\'s, or the record input its workflow takes (nor `approved` beside `review`) \u2014 `options` exactly when it is a select, `entity` exactly when it is a link, and a `default` field holding what it states. A `file` input is read by the Act\'s authored `workflow` alone, starts empty, and is never written into a field by `set`. |\n| `act.review` | `review: true` stands beside an authored `workflow` on an Act over the record, the ticked rows or every row in view, in an App over one entity \u2014 never in an App with Books, nor beside `set`, `template`, `templates`, `intake`, `record`, `sign` or `confirm`, nor in a child List, and it asks inputs only, none named `approved`. Its workflow is that Act\'s alone, run by no other Act. The workflow calls only steps that read \u2014 no agent \u2014 and returns a changeset of the entity\'s table in `data.review`, the same for the same inputs and rows; the platform writes the rows `approved` names, each only while the run proposes it as the caller read it. Over every row in view (`on: "view"`) it is handed no rows and states no `when`: its workflow reads the rows it may change, whatever the view is narrowed to, and no bound on a run\'s rows applies. |\n| `act.set-tagged` | An Act\'s `set` states the moment as `{ "now": true }`, the person pressing as `{ "me": true }` and an input as `{ "input": \u2026 }` \u2014 never the strings `"now"`, `"me"` or `"input:\u2026"`. |\n\n<!-- generated:end rules-act -->\n\n## Template rules\n\nThe rules of the document templates an Act makes and an Export fills.\n\n<!-- generated:start rules-template -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `template.kind` | An Act\'s `template` is an html, excel or word document template and an Export\'s `template` an html or excel one, never an email or pdf-form one. |\n| `template.marker` | Every marker of a Word template an Act makes is filled by that Act: a field of its entity, or a list of the child rows its record lists (the entity\'s own alias over several rows). |\n| `template.act` | *Noted, never refused:* In an App none of whose Acts runs an authored `workflow`, every html, excel or word template is made by an Act (`template`, `templates`) or filled by an Export \u2014 the note names the app over the table whose fields it prints most, where the Act belongs. |\n\n<!-- generated:end rules-template -->\n\n## What the workspace remembers\n\nThe workspace holds every table, field and select option under an id that never changes, whatever its\nlabel or alias becomes. `set_tables` answers each table\'s id, and an\napp\'s body names tables and templates by those ids, so a relabel on either side is a rename and no body\npoints at another table by accident.\n\nA table `set_tables` refuses to write, or an id the workspace no longer holds, is a refusal by name. `restore_table` puts a deleted table back.\n\n## A complete workspace\n\nTwo jobs, so two apps: a sales desk, where an order is taken and shipped, and an account review, where an account\nmanager reads each customer by tier. The split is per job, never per table: a table a job reads only in passing, such as\na price book, is a picker on that job\'s lines or an Act of that job\'s app (`first_build`, \xA7 3).\n\nOne `set_tables` call makes both tables: each item carries a `ref`, and a link names the table it points\nat by the `ref` of the other item. Its answer carries each table\'s id. One `set_app` call then makes each\napp, `{ "body": \u2026 }`: the tree\'s root names the app by its `key` and `name`, and the tables it uses in `tables` by those ids,\nshown here as placeholders to replace.\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": {\n "name": {"label": "Name", "required": true, "type": "text"},\n "logo": {"label": "Logo", "type": "files"},\n "tier": {\n "label": "Tier",\n "default": ["standard"],\n "type": "select",\n "options": [\n {"alias": "standard", "label": "Standard", "color": "slate"},\n {"alias": "gold", "label": "Gold", "color": "amber"}\n ]\n },\n "orders": {"label": "Orders", "type": "link", "pair": "customer", "table": "order", "multi": true},\n "total_ordered": {"label": "Total ordered", "type": "rollup", "via": "orders", "op": "sum", "field": "amount"}\n },\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "field_key": "tier",\n "type": "select",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{"field_key": "name", "order": "asc"}]\n }\n ],\n "title": "name",\n "records": {"image": "logo", "party": "organization", "figure": "total_ordered"}\n },\n {\n "ref": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": {\n "code": {"label": "Order no.", "type": "text", "unique": true},\n "placed_on": {"label": "Placed on", "type": "date", "format": "date"},\n "amount": {"label": "Amount", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "total": {\n "label": "Total with VAT",\n "type": "formula",\n "expression": "{amount} * 1.1",\n "notation": {"style": "currency", "currency": "VND"}\n },\n "customer": {"label": "Customer", "type": "link", "pair": "orders", "table": "customer", "multi": false},\n "note": {"label": "Note", "type": "text"},\n "stage": {\n "label": "Stage",\n "default": ["draft"],\n "type": "select",\n "options": [\n {"alias": "draft", "label": "Draft", "color": "slate"},\n {"alias": "confirmed", "label": "Confirmed", "color": "blue"},\n {"alias": "shipped", "label": "Shipped", "color": "green"}\n ]\n }\n },\n "title": "customer",\n "records": {\n "subtitle": ["code", "placed_on"],\n "figure": "amount",\n "status": {"field": "stage", "closed": ["shipped"]},\n "starts": {"placed_on": "today"}\n }\n }\n ]\n}\n```\n\n```json\n{\n "body": {\n "type": "App",\n "key": "account_review",\n "name": "Account review",\n "color": "blue",\n "tables": {"customer": "<id of the customer table>", "order": "<id of the order table>"},\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "customer",\n "columns": ["tier"],\n "children": [\n {"type": "Reading", "key": "ordered_by_tier", "by": "tier", "value": "total_ordered"},\n {\n "type": "Record",\n "children": [\n {"type": "Section", "key": "account", "title": "Account", "children": ["tier"]},\n {\n "type": "Section",\n "key": "orders",\n "title": "Orders",\n "children": [{"type": "List", "entity": "order", "columns": ["total"], "create": ["code", "amount"]}]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n```json\n{\n "body": {\n "type": "App",\n "key": "sales_desk",\n "name": "Sales desk",\n "color": "violet",\n "tables": {"customer": "<id of the customer table>", "order": "<id of the order table>"},\n "templates": {},\n "children": [\n {\n "type": "List",\n "entity": "order",\n "filters": ["customer"],\n "create": ["code", "customer", "placed_on"],\n "children": [\n {"type": "Reading", "key": "amount_per_period", "by": "placed_on", "value": "amount"},\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {\n "type": "Section",\n "key": "order",\n "title": "Order",\n "children": [\n "note",\n {\n "type": "Act",\n "key": "confirm",\n "label": "Confirm",\n "when": {"stage": ["draft"]},\n "set": {"stage": "confirmed"}\n },\n {\n "type": "Act",\n "key": "ship",\n "label": "Ship",\n "when": {"stage": ["confirmed"]},\n "set": {"stage": "shipped"}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\nThe account review leaves the door a record opens through and the rows it opens in to the system, each the default the\ntable\'s facts give; the sales desk states a drawer. Each List names what its add asks (`create`). The customer\'s orders are a block in its "Orders" section, never its own `Orders`\nlink as a field as well: one fact, one place. An order is titled by its customer and read by its number under it; the\nList names what its add asks, since a subtitle or a figure is never asked by default. `Total ordered` is a rollup, so it\nis read and never asked; the sales desk\'s record opens in a drawer, because its rows are worked one after another. An\norder\'s `stage` is its status (`records.status`), moved only by the app\'s acts: they set it, so the app\'s saves leave it\nout, each act names the stage it is offered at (`when`) and the one it sets, and the register opens on the orders not yet\nshipped (`closed`).\n';
1766
1766
 
1767
1767
  // docs/examples.md
1768
- var examples_default = '# Worked examples\n\nEach section is one treatment, cut from a complete workspace of an invented business: the job, the keys to\nread, where the treatment is wrong, and the excerpt \u2014 the app\'s tree, the App node `set_app` takes as `body`,\nwhich the check passes with no finding; the tables it draws as the items of a `set_tables` call, each with its\n`title` and `records`; those tables\' fields it names, by what each holds and how it is drawn; and the templates it makes.\nLeft out, for the page\'s size: a Section that carries nothing of the treatment, every `label` and `description` of a table or field, `required`, `unique`, `default`, a link\'s\n`pair`, how a computed field computes, the `tables` and `templates` maps an App adds (ids from the\n`set_tables` answer), and template contents \u2014 a call states each table\'s and field\'s `label`. Choosing\namong them: the `design` reference. Every key: the `model` reference, at the pages each section names under\n**Keys**.\n\n## Calendar\n\n**The job.** Book visits on the days they happen, and see a week\'s work at a glance.\n\n**Read.** `layout: "calendar"` on the List; each visit stands over its span because its `records` line holds two dates (`scheduled_for`, `ends_at`).\n\n**Keys.** `model/calendar`\n\n**Not when.** Rows booked on a resource that two may not share at once \u2014 that is `lanes`. Rows with a date nobody plans by (an invoice\'s date) stay a table.\n\n```json\n{\n "tables": [\n {\n "ref": "service_visit",\n "title": "customer",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "scheduled_for": {"type": "date", "format": "datetime"},\n "ends_at": {"type": "date", "format": "datetime"},\n "engineer": {"type": "link", "table": "engineer", "multi": false},\n "job": {\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n "stage": {\n "type": "select",\n "options": [\n {"alias": "booked_on", "color": "blue"},\n {"alias": "parts_ready_on", "color": "blue"},\n {"alias": "arrived_on", "color": "blue"},\n {"alias": "completed_on", "color": "green"}\n ]\n },\n "booked_on": {"type": "date"},\n "parts_ready_on": {"type": "date"},\n "arrived_on": {"type": "date"},\n "hours": {"type": "number", "notation": {"style": "unit", "unit": "h"}},\n "report": {"type": "text", "format": "markdown"},\n "completed_on": {"type": "date"}\n },\n "records": {\n "subtitle": ["scheduled_for", "ends_at"],\n "figure": "hours",\n "due": ["scheduled_for"],\n "status": {\n "field": "stage",\n "closed": ["completed_on"],\n "dates": {\n "booked_on": "booked_on",\n "parts_ready_on": "parts_ready_on",\n "arrived_on": "arrived_on",\n "completed_on": "completed_on"\n }\n }\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "visits",\n "name": "Service visits",\n "icon": "calendar",\n "color": "emerald",\n "children": [\n {\n "type": "List",\n "entity": "service_visit",\n "row": {"title": {"add": ["name", "credit_limit"]}},\n "columns": ["engineer", "job"],\n "filters": ["customer"],\n "sort": {"field": "scheduled_for"},\n "layout": "calendar",\n "create": [\n "customer",\n "job",\n "scheduled_for",\n "ends_at",\n {"field": "engineer", "add": ["name", {"field": "base", "add": ["name", "city"]}]}\n ],\n "children": [\n {"type": "Reading", "key": "register_reading_0", "label": "Repairs", "where": {"job": ["repair"]}},\n {"type": "Reading", "key": "register_reading_1", "value": "hours", "by": "engineer"},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "visit",\n "title": "Visit",\n "at": ["booked_on", "parts_ready_on"],\n "children": [\n "job",\n "booked_on",\n "parts_ready_on",\n {\n "type": "Act",\n "key": "reschedule",\n "label": "Reschedule",\n "asks": ["scheduled_for", "ends_at"],\n "when": {"stage": ["booked_on", "parts_ready_on"]}\n },\n {\n "type": "Act",\n "key": "to_booked_on",\n "label": "Booked",\n "when": {"stage": {"empty": true}},\n "asks": [{"field": "booked_on", "starts": "today"}],\n "set": {"stage": "booked_on"}\n },\n {\n "type": "Act",\n "key": "to_parts_ready_on",\n "label": "Parts ready",\n "when": {"stage": ["booked_on"], "job": ["repair"]},\n "asks": [{"field": "parts_ready_on", "starts": "today"}],\n "set": {"stage": "parts_ready_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on",\n "label": "On site",\n "when": {"stage": ["booked_on"], "job": ["inspection"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on_2",\n "label": "On site",\n "when": {"stage": ["parts_ready_on"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "report",\n "title": "Report",\n "at": ["arrived_on"],\n "children": [\n "engineer",\n "arrived_on",\n {"field": "report", "block": false},\n {\n "type": "Act",\n "key": "complete",\n "label": "Complete visit",\n "requires": ["engineer"],\n "asks": [\n "hours",\n {"input": "summary", "label": "What was done", "type": "long_text", "required": true}\n ],\n "set": {"report": {"input": "summary"}, "stage": "completed_on"},\n "when": {"stage": ["arrived_on"]}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Lanes\n\n**The job.** Put each service visit on an engineer\'s day by the hour, and see at once who is free between visits.\n\n**Read.** `layout` `lanes` with `by` naming the one-row link to the resource (the engineer); every engineer stands as a lane, empty or not.\n\n**Keys.** `model/lanes`\n\n**Not when.** The resource is not a row of the model (a free-text room name), or rows never compete for one \u2014 a `calendar` reads them by day.\n\n```json\n{\n "tables": [\n {\n "ref": "service_visit",\n "title": "customer",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "scheduled_for": {"type": "date", "format": "datetime"},\n "ends_at": {"type": "date", "format": "datetime"},\n "engineer": {"type": "link", "table": "engineer", "multi": false},\n "job": {\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n "stage": {\n "type": "select",\n "options": [\n {"alias": "booked_on", "color": "blue"},\n {"alias": "parts_ready_on", "color": "blue"},\n {"alias": "arrived_on", "color": "blue"},\n {"alias": "completed_on", "color": "green"}\n ]\n },\n "booked_on": {"type": "date"},\n "parts_ready_on": {"type": "date"},\n "arrived_on": {"type": "date"},\n "hours": {"type": "number", "notation": {"style": "unit", "unit": "h"}},\n "completed_on": {"type": "date"}\n },\n "records": {\n "subtitle": ["scheduled_for", "ends_at"],\n "figure": "hours",\n "due": ["scheduled_for"],\n "status": {\n "field": "stage",\n "closed": ["completed_on"],\n "dates": {\n "booked_on": "booked_on",\n "parts_ready_on": "parts_ready_on",\n "arrived_on": "arrived_on",\n "completed_on": "completed_on"\n }\n }\n }\n },\n {\n "ref": "engineer",\n "title": "name",\n "fields": {"name": {"type": "text"}, "base": {"type": "link", "table": "warehouse", "multi": false}},\n "records": {"subtitle": ["base"], "party": "person"}\n }\n ],\n "body": {\n "type": "App",\n "key": "dispatch",\n "name": "Dispatch",\n "icon": "wrench",\n "color": "sky",\n "children": [\n {\n "type": "List",\n "entity": "service_visit",\n "row": {\n "title": {"add": ["name", "credit_limit"]},\n "subtitle": [{"field": "scheduled_for", "edit": false}, {"field": "ends_at", "edit": false}]\n },\n "columns": ["job"],\n "search": ["customer"],\n "layout": {"lanes": {"by": "engineer"}},\n "create": [\n {"field": "engineer", "add": ["name", {"field": "base", "add": ["name", "city"]}]},\n "scheduled_for",\n "ends_at",\n "customer",\n "job"\n ],\n "children": [\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {\n "type": "Section",\n "key": "booking",\n "title": "Booking",\n "children": [\n "engineer",\n "job",\n {\n "type": "Act",\n "key": "to_booked_on",\n "label": "Booked",\n "when": {"stage": {"empty": true}},\n "asks": [{"field": "booked_on", "starts": "today"}],\n "set": {"stage": "booked_on"}\n },\n {\n "type": "Act",\n "key": "to_parts_ready_on",\n "label": "Parts ready",\n "when": {"stage": ["booked_on"], "job": ["repair"]},\n "asks": [{"field": "parts_ready_on", "starts": "today"}],\n "set": {"stage": "parts_ready_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on",\n "label": "On site",\n "when": {"stage": ["booked_on"], "job": ["inspection"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on_2",\n "label": "On site",\n "when": {"stage": ["parts_ready_on"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n },\n {\n "type": "Act",\n "key": "to_completed_on",\n "label": "Completed",\n "when": {"stage": ["arrived_on"]},\n "asks": [{"field": "completed_on", "starts": "today"}],\n "set": {"stage": "completed_on"}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Gantt\n\n**The job.** Read every hire as a bar from the day the machine goes out to the day it is due back, against today.\n\n**Read.** `layout` `gantt` with its `start`; with no end stated, the bar ends at the entity\'s first `due`. The day a hire comes back (`ngay_tra`) is stamped by the status\'s `dates`, so no act sets it.\n\n**Keys.** `model/gantt`\n\n**Not when.** Rows that last a day or less (a visit, a shift) \u2014 `calendar` or `lanes`; a plan with no dates set yet.\n\n```json\n{\n "tables": [\n {\n "ref": "phieu_thue",\n "title": "khach_hang",\n "fields": {\n "khach_hang": {"type": "link", "table": "khach_hang", "multi": false},\n "so_phieu": {"type": "text"},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "moi", "color": "slate"},\n {"alias": "dang_thue", "color": "blue"},\n {"alias": "da_tra", "color": "green"},\n {"alias": "huy", "color": "gray"}\n ]\n },\n "phu_trach": {"type": "member"},\n "ngay_giao": {"type": "date"},\n "han_tra": {"type": "date"},\n "ngay_tra": {"type": "date"},\n "tinh_trang_tra": {\n "type": "select",\n "options": [\n {"alias": "tot", "color": "green"},\n {"alias": "tray_xuoc", "color": "amber"},\n {"alias": "hu_hong", "color": "red"}\n ]\n },\n "so_ngay": {"type": "formula"},\n "tong_tien": {"type": "rollup"}\n },\n "records": {\n "subtitle": ["so_phieu"],\n "figure": "tong_tien",\n "due": ["han_tra"],\n "status": {"field": "trang_thai", "closed": ["da_tra", "huy"], "dates": {"da_tra": "ngay_tra"}}\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "tien_do_thue",\n "name": "Ti\u1EBFn \u0111\u1ED9 cho thu\xEA",\n "icon": "calendar-range",\n "color": "teal",\n "children": [\n {\n "type": "List",\n "entity": "phieu_thue",\n "row": {"title": {"add": true}},\n "columns": ["phu_trach"],\n "filters": ["phu_trach", "khach_hang"],\n "layout": {"gantt": {"start": "ngay_giao"}},\n "create": false,\n "remove": false,\n "children": [\n {\n "type": "Reading",\n "key": "register_reading_0",\n "label": "Phi\u1EBFu \u0111ang cho thu\xEA",\n "where": {"trang_thai": ["dang_thue"]}\n },\n {\n "type": "Reading",\n "key": "register_reading_1",\n "label": "Ti\u1EC1n thu\xEA \u0111ang ch\u1EA1y",\n "value": "tong_tien",\n "where": {"trang_thai": ["dang_thue"]}\n },\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "phieu_thue",\n "title": "Phi\u1EBFu thu\xEA",\n "children": ["ngay_giao", "han_tra"]\n },\n {\n "type": "Section",\n "key": "dang_thue",\n "title": "\u0110ang thu\xEA",\n "at": ["dang_thue"],\n "children": [\n "so_ngay",\n "ngay_tra",\n {\n "type": "Act",\n "key": "nhan_tra",\n "label": "Nh\u1EADn m\xE1y tr\u1EA3",\n "when": {"trang_thai": ["dang_thue"]},\n "asks": ["tinh_trang_tra"],\n "set": {"trang_thai": "da_tra"},\n "confirm": true\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Roster\n\n**The job.** See who works which shift on each day of the week or the month.\n\n**Read.** `layout: "roster"` over the people; the child List (`lich_truc`) is the rows that fill the days \u2014 one link back and a date on its line.\n\n**Keys.** `model/roster`\n\n**Not when.** Rows that are themselves the bookings (a stay over several days) \u2014 a `calendar` or `lanes` draws them; a roster\'s row is what the days are filled for.\n\n```json\n{\n "tables": [\n {\n "ref": "nha_si",\n "title": "ho_ten",\n "fields": {\n "ho_ten": {"type": "text"},\n "chuyen_mon": {\n "type": "select",\n "options": [\n {"alias": "tong_quat", "color": "blue"},\n {"alias": "chinh_nha", "color": "purple"},\n {"alias": "nha_chu", "color": "green"},\n {"alias": "phuc_hinh", "color": "orange"}\n ]\n },\n "dien_thoai": {"type": "text"}\n },\n "records": {"subtitle": ["chuyen_mon"], "party": "person"}\n },\n {\n "ref": "lich_truc",\n "title": "ca",\n "fields": {\n "nha_si": {"type": "link", "table": "nha_si", "multi": false},\n "ca": {\n "type": "select",\n "options": [\n {"alias": "sang", "color": "blue"},\n {"alias": "chieu", "color": "purple"},\n {"alias": "ca_ngay", "color": "green"},\n {"alias": "nghi", "color": "gray"}\n ]\n },\n "ngay": {"type": "date"},\n "ghe": {"type": "link", "table": "ghe", "multi": false}\n },\n "records": {"subtitle": ["ngay", "ghe"]}\n }\n ],\n "body": {\n "type": "App",\n "key": "lich_truc",\n "name": "L\u1ECBch tr\u1EF1c",\n "icon": "users",\n "color": "indigo",\n "children": [\n {\n "type": "List",\n "entity": "nha_si",\n "filters": ["chuyen_mon"],\n "layout": "roster",\n "create": ["ho_ten", "chuyen_mon", "dien_thoai"],\n "children": [\n {\n "type": "List",\n "entity": "lich_truc",\n "row": {"subtitle": ["ngay", {"field": "ghe", "add": ["ten", "phong"]}]},\n "create": true,\n "remove": true\n }\n ]\n }\n ]\n }\n}\n```\n\n## Cards\n\n**The job.** Find an item by its picture, and count what the shelf holds.\n\n**Read.** `layout: "cards"` over rows whose `records.image` is a photo; `scope` keeps the app inside one warehouse at a time.\n\n**Keys.** `model/cards`, `model/records`, `model/app-node`\n\n**Not when.** Rows with no picture \u2014 cards of words are a table drawn worse.\n\n```json\n{\n "tables": [\n {\n "ref": "stock_item",\n "title": "name",\n "fields": {\n "name": {"type": "text"},\n "warehouse": {"type": "link", "table": "warehouse", "multi": false},\n "photo": {"type": "files"},\n "sku": {"type": "text"},\n "category": {\n "type": "select",\n "options": [\n {"alias": "pumps", "color": "blue"},\n {"alias": "valves", "color": "teal"},\n {"alias": "seals", "color": "amber"},\n {"alias": "motors", "color": "violet"}\n ]\n },\n "on_hand": {"type": "number"},\n "minimum": {"type": "number"},\n "price": {"type": "number", "notation": {"style": "currency", "currency": "USD"}},\n "supplier_page": {"type": "text", "format": "link"},\n "last_counted": {"type": "date"},\n "counted_by": {"type": "member"},\n "count_note": {"type": "text"}\n },\n "records": {\n "subtitle": ["sku", "category"],\n "image": "photo",\n "figure": "on_hand",\n "limits": {"on_hand": 400},\n "gates": {"on_hand": "minimum"}\n }\n },\n {\n "ref": "order_line",\n "title": "item",\n "fields": {\n "item": {"type": "link", "table": "stock_item", "multi": false},\n "order": {"type": "link", "table": "order", "multi": false},\n "quantity": {"type": "number"},\n "shipped": {"type": "number"},\n "picked": {"type": "number"},\n "unit_price": {"type": "number", "notation": {"style": "currency", "currency": "USD"}},\n "amount": {"type": "formula", "notation": {"style": "currency", "currency": "USD"}},\n "settled": {"type": "formula"},\n "category": {"type": "lookup"}\n },\n "records": {\n "subtitle": ["order", "unit_price"],\n "figure": "amount",\n "limits": {"shipped": "quantity", "picked": "quantity"}\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "stock",\n "name": "Stock",\n "icon": "boxes",\n "color": "teal",\n "scope": "warehouse",\n "children": [\n {\n "type": "List",\n "entity": "stock_item",\n "columns": ["price"],\n "filters": ["on_hand"],\n "search": ["name", "sku"],\n "layout": "cards",\n "children": [\n {"type": "Reading", "key": "register_reading_0", "value": "on_hand", "by": "category"},\n {"type": "Export", "key": "export"},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "item",\n "title": "Item",\n "children": ["price", "minimum", "supplier_page"]\n },\n {\n "type": "Section",\n "key": "last_count",\n "title": "Last count",\n "children": [\n "last_counted",\n "counted_by",\n "count_note",\n {\n "type": "Act",\n "key": "count",\n "label": "Count stock",\n "asks": [\n {"input": "counted", "label": "Units on the shelf", "type": "number", "required": true},\n {"input": "remark", "label": "Remark", "type": "long_text"}\n ],\n "set": {\n "on_hand": {"input": "counted"},\n "last_counted": {"now": true},\n "counted_by": {"me": true},\n "count_note": {"input": "remark"}\n }\n }\n ]\n },\n {\n "type": "Section",\n "key": "on_order",\n "title": "On order",\n "children": [\n {\n "type": "List",\n "entity": "order_line",\n "row": {\n "subtitle": [\n {\n "field": "order",\n "add": [{"field": "customer", "add": ["name", "credit_limit"]}, "reference"]\n },\n "unit_price"\n ]\n },\n "columns": ["quantity"],\n "children": [\n {"type": "Check", "field": "settled", "on": ["update"]},\n {"type": "Record", "children": ["item", "order", "quantity", "shipped", "unit_price"]}\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Party\n\n**The job.** Keep each customer account \u2014 who they are, what they ordered, what was said to them.\n\n**Read.** `records.customer.party: "organization"` and its `image` (the logo); a List with `layout: "timeline"` for the touches, a Reading `by` the day placed for its orders, and its papers `expect`ed by kind.\n\n**Keys.** `model/records`, `model/layouts`, `model/list-node`, `model/reading-node`\n\n**Not when.** A list of customers is not a job by itself: the customer is a link on the desk whose rows name it, unless the job is the account itself.\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "title": "name",\n "fields": {\n "name": {"type": "text"},\n "logo": {"type": "files"},\n "code": {"type": "text"},\n "tier": {\n "type": "select",\n "options": [\n {"alias": "key", "color": "violet"},\n {"alias": "standard", "color": "sky"},\n {"alias": "lapsed", "color": "gray"}\n ]\n },\n "phone": {"type": "text"},\n "website": {"type": "text", "format": "link"},\n "account_manager": {"type": "member"},\n "credit_limit": {"type": "number", "notation": {"style": "currency", "currency": "USD"}},\n "orders": {"type": "link", "table": "order", "multi": true},\n "outstanding": {"type": "rollup"},\n "over_limit": {"type": "formula"},\n "papers_needed": {\n "type": "select",\n "options": [\n {"alias": "credit_application", "color": "blue"},\n {"alias": "trade_reference", "color": "violet"},\n {"alias": "vat_certificate", "color": "teal"}\n ],\n "multi": true\n },\n "notes": {"type": "text", "format": "markdown"}\n },\n "records": {\n "subtitle": ["tier"],\n "image": "logo",\n "party": "organization",\n "figure": "outstanding",\n "limits": {"outstanding": "credit_limit"}\n }\n },\n {\n "ref": "order",\n "title": "reference",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "reference": {"type": "text"},\n "stage": {\n "type": "select",\n "options": [\n {"alias": "draft", "color": "slate"},\n {"alias": "confirmed", "color": "blue"},\n {"alias": "picking", "color": "amber"},\n {"alias": "shipped", "color": "indigo"},\n {"alias": "received", "color": "green"},\n {"alias": "cancelled", "color": "rose"}\n ]\n },\n "placed": {"type": "date"},\n "due_date": {"type": "date"},\n "handler": {"type": "member"},\n "priority": {"type": "boolean"},\n "lines": {"type": "link", "table": "order_line", "multi": true},\n "payments": {"type": "link", "table": "payment", "multi": true},\n "total": {"type": "rollup"},\n "balance": {"type": "formula", "notation": {"style": "currency", "currency": "USD"}},\n "units_ordered": {"type": "rollup"},\n "units_shipped": {"type": "rollup"},\n "units_picked": {"type": "rollup"},\n "carrier": {"type": "text"},\n "tracking_no": {"type": "text"},\n "shipped_on": {"type": "date"},\n "cancellation_reason": {"type": "text"},\n "cancelled_by": {"type": "member"},\n "replaced_by": {"type": "link", "table": "order", "multi": false},\n "attachments": {"type": "files"},\n "delivery_note": {"type": "files"},\n "notes": {"type": "text", "format": "markdown"},\n "paid_in_full": {"type": "formula"},\n "has_payments": {"type": "formula"},\n "reminder_ref": {"type": "text"}\n },\n "records": {\n "subtitle": ["customer", "placed"],\n "figure": "total",\n "limits": {"units_shipped": "units_ordered", "units_picked": "units_ordered"},\n "due": ["due_date"],\n "status": {\n "field": "stage",\n "closed": ["received", "cancelled"],\n "dates": {"shipped": "shipped_on"},\n "history": "order_stage"\n }\n }\n },\n {\n "ref": "customer_paper",\n "title": "kind",\n "fields": {\n "kind": {\n "type": "select",\n "options": [\n {"alias": "credit_application", "color": "blue"},\n {"alias": "trade_reference", "color": "violet"},\n {"alias": "vat_certificate", "color": "teal"}\n ]\n },\n "customer": {"type": "link", "table": "customer", "multi": false},\n "received_on": {"type": "date"},\n "file": {"type": "files"}\n },\n "records": {"image": "file"}\n },\n {\n "ref": "touch",\n "title": "said",\n "fields": {\n "said": {"type": "text"},\n "customer": {"type": "link", "table": "customer", "multi": false},\n "on": {"type": "date"},\n "by": {"type": "member"}\n }\n },\n {\n "ref": "service_visit",\n "title": "customer",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "scheduled_for": {"type": "date", "format": "datetime"},\n "ends_at": {"type": "date", "format": "datetime"},\n "engineer": {"type": "link", "table": "engineer", "multi": false},\n "job": {\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n "stage": {\n "type": "select",\n "options": [\n {"alias": "booked_on", "color": "blue"},\n {"alias": "parts_ready_on", "color": "blue"},\n {"alias": "arrived_on", "color": "blue"},\n {"alias": "completed_on", "color": "green"}\n ]\n },\n "booked_on": {"type": "date"},\n "parts_ready_on": {"type": "date"},\n "arrived_on": {"type": "date"},\n "hours": {"type": "number", "notation": {"style": "unit", "unit": "h"}},\n "report": {"type": "text", "format": "markdown"},\n "completed_on": {"type": "date"}\n },\n "records": {\n "subtitle": ["scheduled_for", "ends_at"],\n "figure": "hours",\n "due": ["scheduled_for"],\n "status": {\n "field": "stage",\n "closed": ["completed_on"],\n "dates": {\n "booked_on": "booked_on",\n "parts_ready_on": "parts_ready_on",\n "arrived_on": "arrived_on",\n "completed_on": "completed_on"\n }\n }\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "customers",\n "name": "Customers",\n "icon": "users",\n "color": "violet",\n "children": [\n {\n "type": "List",\n "entity": "customer",\n "columns": ["phone", "account_manager"],\n "filters": ["account_manager"],\n "sort": {"field": "name"},\n "opens": {"tier": ["key", "standard"]},\n "create": ["name", "code", "credit_limit"],\n "children": [\n {"type": "Reading", "key": "register_reading_0", "value": "outstanding", "by": "tier"},\n {"type": "Reading", "key": "register_reading_1", "value": "outstanding", "by": "account_manager"},\n {"type": "Check", "field": "over_limit"},\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {"type": "Section", "key": "contact", "title": "Contact", "children": ["phone", "website"]},\n {\n "type": "Section",\n "key": "account",\n "title": "Account",\n "children": [\n "code",\n "account_manager",\n "credit_limit",\n {\n "type": "Act",\n "key": "reassign",\n "label": "Reassign",\n "asks": [{"input": "to", "label": "Account manager", "type": "member", "required": true}],\n "set": {"account_manager": {"input": "to"}}\n },\n {\n "type": "Act",\n "key": "lapse",\n "label": "Mark lapsed",\n "when": {"tier": ["key", "standard"]},\n "set": {"tier": "lapsed"},\n "confirm": true\n },\n {\n "type": "Act",\n "key": "reinstate",\n "label": "Reinstate",\n "when": {"tier": ["lapsed"]},\n "asks": [\n {\n "input": "back_to",\n "label": "Back to",\n "type": "select",\n "options": [\n {"alias": "key", "label": "Key account", "color": "violet"},\n {"alias": "standard", "label": "Standard", "color": "sky"}\n ],\n "required": true\n }\n ],\n "set": {"tier": {"input": "back_to"}}\n }\n ]\n },\n {\n "type": "Section",\n "key": "orders",\n "title": "Orders",\n "children": [\n {\n "type": "Reading",\n "key": "block_reading_0",\n "label": "Order value",\n "entity": "order",\n "value": "total",\n "where": {"stage": ["confirmed", "picking", "shipped", "received"]},\n "by": "placed"\n },\n {\n "type": "List",\n "entity": "order",\n "where": {"stage": ["draft", "confirmed", "picking", "shipped"]},\n "label": "Open orders",\n "columns": ["due_date"],\n "create": false,\n "children": [\n {"type": "Check", "field": "paid_in_full", "on": ["update"]},\n {"type": "Check", "field": "has_payments"},\n {\n "type": "Record",\n "children": [\n "due_date",\n "handler",\n {"field": "priority", "edit": false},\n {\n "field": "lines",\n "add": [\n {\n "field": "item",\n "add": ["name", {"field": "warehouse", "add": ["name", "city"]}, "sku", "category"]\n },\n "quantity"\n ]\n },\n {"field": "payments", "add": true},\n {"field": "carrier", "edit": false},\n {"field": "tracking_no", "edit": false},\n {"field": "shipped_on", "edit": false},\n {"field": "cancellation_reason", "edit": false},\n {"field": "cancelled_by", "edit": false},\n {"field": "replaced_by", "edit": false},\n {"field": "attachments", "edit": false},\n "delivery_note",\n "notes",\n {"field": "reminder_ref", "edit": false}\n ]\n }\n ]\n },\n {\n "type": "List",\n "key": "order_2",\n "entity": "order",\n "where": {"stage": ["shipped", "received"], "paid_in_full": false},\n "label": "Shipped, still owed",\n "columns": ["balance"],\n "create": false,\n "children": [{"type": "Record", "children": ["due_date"]}]\n },\n {\n "type": "List",\n "key": "block_reading_3",\n "entity": "service_visit",\n "app": "visits",\n "label": "Service visits",\n "columns": ["engineer", "job"],\n "children": [\n {\n "type": "Act",\n "key": "clear_visit_hours",\n "label": "Clear hours",\n "set": {"hours": null},\n "confirm": true\n },\n {"type": "Record", "edit": false, "children": ["report", "arrived_on", "completed_on"]}\n ]\n }\n ]\n },\n {\n "type": "Section",\n "key": "account_papers",\n "title": "Account papers",\n "children": [\n {\n "type": "List",\n "entity": "customer_paper",\n "expect": {"field": "kind", "of": "papers_needed"},\n "columns": ["received_on"]\n }\n ]\n },\n {\n "type": "Section",\n "key": "contact_log",\n "title": "Contact log",\n "children": [{"type": "List", "entity": "touch", "layout": "timeline"}]\n },\n {"type": "Section", "key": "notes", "title": "Notes", "children": ["notes"]}\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Case\n\n**The job.** Gather an applicant\'s papers into one case, make the forms it needs, and file it on time.\n\n**Read.** Lists with `expect` for the papers brought (one line per kind, the missing ones empty), an Act\'s `templates` for the forms made (ticked and made at once, kept in `into`), and `intake` reading the papers brought.\n\n**Keys.** `model/list-node`, `model/act-node`\n\n**Not when.** A record that needs no papers, or one paper made once \u2014 a single `template` act.\n\n```json\n{\n "tables": [\n {\n "ref": "ho_so",\n "title": "khach_hang",\n "fields": {\n "khach_hang": {"type": "link", "table": "khach_hang", "multi": false},\n "du_an": {"type": "link", "table": "du_an", "multi": false},\n "ma_ho_so": {"type": "autonumber"},\n "dich_vu": {\n "type": "select",\n "options": [{"alias": "tron_goi", "color": "blue"}, {"alias": "lam_ho_so", "color": "violet"}]\n },\n "doi_tuong": {\n "type": "select",\n "options": [\n {"alias": "thu_nhap_thap", "color": "sky"},\n {"alias": "cong_nhan", "color": "teal"},\n {"alias": "can_bo", "color": "indigo"},\n {"alias": "luc_luong", "color": "green"},\n {"alias": "ho_ngheo", "color": "amber"},\n {"alias": "nguoi_co_cong", "color": "rose"}\n ]\n },\n "tinh_trang_nha_o": {\n "type": "select",\n "options": [{"alias": "chua_co_nha", "color": "gray"}, {"alias": "nha_chat", "color": "orange"}]\n },\n "giay_chung_nhan_so": {"type": "text"},\n "dien_tich_nha": {"type": "number"},\n "sale": {"type": "member"},\n "xu_ly": {"type": "member"},\n "can_bo_sung": {"type": "boolean"},\n "phi_dich_vu": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "loai_can": {\n "type": "select",\n "options": [\n {"alias": "studio", "color": "sky"},\n {"alias": "mot_pn", "color": "blue"},\n {"alias": "hai_pn", "color": "indigo"},\n {"alias": "ba_pn", "color": "violet"}\n ]\n },\n "giai_doan": {\n "type": "select",\n "options": [\n {"alias": "ngay_thu_phi", "color": "blue"},\n {"alias": "ngay_nhan_giay_to", "color": "blue"},\n {"alias": "ngay_lap_ho_so", "color": "blue"},\n {"alias": "ngay_nop_cdt", "color": "blue"},\n {"alias": "ngay_cdt_duyet", "color": "blue"},\n {"alias": "ngay_so_xd_duyet", "color": "blue"},\n {"alias": "ngay_boc_tham", "color": "blue"},\n {"alias": "ngay_ky_hdmb", "color": "green"},\n {"alias": "ngay_ban_giao", "color": "green"}\n ]\n },\n "ngay_thu_phi": {"type": "date"},\n "ngay_nhan_giay_to": {"type": "date"},\n "ngay_lap_ho_so": {"type": "date"},\n "ngay_nop_cdt": {"type": "date"},\n "ngay_cdt_duyet": {"type": "date"},\n "ngay_so_xd_duyet": {"type": "date"},\n "ngay_boc_tham": {"type": "date"},\n "ngay_ky_hdmb": {"type": "date"},\n "ngay_ban_giao": {"type": "date"},\n "bo_ho_so": {"type": "files"},\n "giay_to": {"type": "link", "table": "giay_to", "multi": true},\n "thanh_vien": {"type": "link", "table": "thanh_vien", "multi": true},\n "phieu_thu": {"type": "link", "table": "phieu_thu", "multi": true},\n "dien_thoai": {"type": "lookup"},\n "cccd": {"type": "lookup"},\n "ngay_sinh": {"type": "lookup"},\n "dia_chi": {"type": "lookup"},\n "da_thu": {"type": "rollup"},\n "chua_thu_phi": {"type": "formula"},\n "con_no_phi": {"type": "formula"},\n "vuot_thu_nhap": {"type": "formula"},\n "da_hoan_tat": {"type": "formula"}\n },\n "records": {\n "subtitle": ["dich_vu"],\n "figure": "phi_dich_vu",\n "status": {\n "field": "giai_doan",\n "closed": ["ngay_ky_hdmb", "ngay_ban_giao"],\n "dates": {\n "ngay_thu_phi": "ngay_thu_phi",\n "ngay_nhan_giay_to": "ngay_nhan_giay_to",\n "ngay_lap_ho_so": "ngay_lap_ho_so",\n "ngay_nop_cdt": "ngay_nop_cdt",\n "ngay_cdt_duyet": "ngay_cdt_duyet",\n "ngay_so_xd_duyet": "ngay_so_xd_duyet",\n "ngay_boc_tham": "ngay_boc_tham",\n "ngay_ky_hdmb": "ngay_ky_hdmb",\n "ngay_ban_giao": "ngay_ban_giao"\n }\n }\n }\n },\n {\n "ref": "thanh_vien",\n "title": "ho_ten",\n "fields": {\n "ho_ten": {"type": "text"},\n "ho_so": {"type": "link", "table": "ho_so", "multi": false},\n "quan_he": {\n "type": "select",\n "options": [\n {"alias": "nguoi_dung_don", "color": "blue"},\n {"alias": "vo_chong", "color": "violet"},\n {"alias": "con", "color": "teal"},\n {"alias": "bo_me", "color": "amber"},\n {"alias": "khac", "color": "gray"}\n ]\n },\n "ngay_sinh": {"type": "date"},\n "cccd": {"type": "text"},\n "thu_nhap": {"type": "number", "notation": {"style": "currency", "currency": "VND"}}\n },\n "records": {"subtitle": ["quan_he", "cccd"], "party": "person", "figure": "thu_nhap"}\n },\n {\n "ref": "giay_to",\n "title": "loai",\n "fields": {\n "loai": {\n "type": "select",\n "options": [\n {"alias": "don_dang_ky", "color": "blue"},\n {"alias": "can_cuoc", "color": "sky"},\n {"alias": "giay_doi_tuong", "color": "violet"},\n {"alias": "xac_nhan_nha_o", "color": "teal"},\n {"alias": "xac_nhan_thu_nhap", "color": "amber"},\n {"alias": "hon_nhan", "color": "indigo"}\n ]\n },\n "ho_so": {"type": "link", "table": "ho_so", "multi": false},\n "ngay_nhan": {"type": "date"},\n "tep": {"type": "files"},\n "phai_sua": {"type": "formula"}\n },\n "records": {"image": "tep", "starts": {"ngay_nhan": "today"}}\n },\n {\n "ref": "phieu_thu",\n "title": "khoan",\n "fields": {\n "khoan": {\n "type": "select",\n "options": [\n {"alias": "dat_coc", "color": "blue"},\n {"alias": "tat_toan", "color": "indigo"},\n {"alias": "phi_ho_so", "color": "violet"}\n ]\n },\n "ho_so": {"type": "link", "table": "ho_so", "multi": false},\n "so_tien": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "hinh_thuc": {\n "type": "select",\n "options": [\n {"alias": "tien_mat", "color": "green", "mark": {"kind": "icon", "name": "banknote"}},\n {"alias": "chuyen_khoan", "color": "blue", "mark": {"kind": "icon", "name": "arrow-right-left"}}\n ]\n },\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "cho_thu", "color": "amber"},\n {"alias": "da_thu", "color": "green"},\n {"alias": "huy", "color": "gray"}\n ]\n },\n "ngay_thu": {"type": "date"}\n },\n "records": {\n "subtitle": ["hinh_thuc", "ngay_thu"],\n "figure": "so_tien",\n "status": {"field": "trang_thai", "closed": ["da_thu", "huy"]}\n }\n }\n ],\n "templates": [\n {"alias": "don_dang_ky", "type": "html", "label": "\u0110\u01A1n \u0111\u0103ng k\xFD mua nh\xE0 \u1EDF x\xE3 h\u1ED9i"},\n {"alias": "mau_02_chua_co_nha", "type": "html", "label": "M\u1EABu 02 \u2014 X\xE1c nh\u1EADn ch\u01B0a c\xF3 nh\xE0 \u1EDF"},\n {"alias": "mau_03_nha_chat", "type": "html", "label": "M\u1EABu 03 \u2014 X\xE1c nh\u1EADn nh\xE0 \u1EDF d\u01B0\u1EDBi 15 m\xB2/ng\u01B0\u1EDDi"},\n {"alias": "mau_05_thu_nhap", "type": "html", "label": "M\u1EABu 05 \u2014 X\xE1c nh\u1EADn thu nh\u1EADp"},\n {"alias": "danh_sach_nop", "type": "html", "label": "Danh s\xE1ch h\u1ED3 s\u01A1 n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0"}\n ],\n "body": {\n "type": "App",\n "key": "ho_so",\n "name": "H\u1ED3 s\u01A1 nh\xE0 \u1EDF x\xE3 h\u1ED9i",\n "icon": "folder-open",\n "color": "blue",\n "scope": "du_an",\n "children": [\n {\n "type": "List",\n "entity": "ho_so",\n "row": {"title": {"add": true}},\n "columns": ["xu_ly", "can_bo_sung", "doi_tuong"],\n "filters": ["xu_ly", "can_bo_sung"],\n "search": ["khach_hang", "ma_ho_so", "dien_thoai"],\n "create": ["khach_hang", "dich_vu", "doi_tuong", "tinh_trang_nha_o", "loai_can", "phi_dich_vu", "sale"],\n "children": [\n {\n "type": "Reading",\n "key": "register_reading_0",\n "label": "Ch\u1EDD kh\xE1ch b\u1ED5 sung",\n "where": {"can_bo_sung": true}\n },\n {\n "type": "Reading",\n "key": "register_reading_1",\n "label": "Ph\xED \u0111\xE3 thu",\n "value": "da_thu",\n "over": "ngay_nhan_giay_to"\n },\n {"type": "Reading", "key": "register_reading_2", "by": "doi_tuong"},\n {"type": "Export", "key": "export"},\n {"type": "Check", "field": "chua_thu_phi", "on": ["nop_cdt"]},\n {"type": "Check", "field": "con_no_phi"},\n {"type": "Check", "field": "vuot_thu_nhap"},\n {"type": "Check", "field": "da_hoan_tat", "on": ["doc_giay_to"]},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "nguoi_dung_don",\n "title": "Ng\u01B0\u1EDDi \u0111\u1EE9ng \u0111\u01A1n",\n "children": [\n "ma_ho_so",\n "dien_thoai",\n "cccd",\n "ngay_sinh",\n "dia_chi",\n "doi_tuong",\n "tinh_trang_nha_o",\n "sale",\n "xu_ly",\n "can_bo_sung",\n "ngay_ky_hdmb",\n "ngay_ban_giao",\n {\n "type": "List",\n "entity": "thanh_vien",\n "label": "H\u1ED9 gia \u0111\xECnh",\n "columns": ["ngay_sinh"],\n "create": ["ho_ten", "quan_he", "ngay_sinh", "cccd", "thu_nhap"]\n },\n {\n "type": "List",\n "entity": "giay_to",\n "expect": {"field": "loai", "only": ["can_cuoc", "hon_nhan"]},\n "label": "Gi\u1EA5y t\u1EDD c\xE1 nh\xE2n",\n "columns": ["ngay_nhan"],\n "children": [{"type": "Check", "field": "phai_sua"}]\n },\n {\n "type": "Act",\n "key": "doc_giay_to",\n "label": "\u0110\u1ECDc gi\u1EA5y t\u1EDD",\n "intake": "giay_to",\n "fills": [\n "doi_tuong",\n "tinh_trang_nha_o",\n "giay_chung_nhan_so",\n "dien_tich_nha",\n "khach_hang.ho_ten",\n "khach_hang.cccd",\n "khach_hang.ngay_sinh",\n "khach_hang.dia_chi",\n "thanh_vien"\n ]\n },\n {\n "type": "Act",\n "key": "giao_xu_ly",\n "label": "Giao ng\u01B0\u1EDDi x\u1EED l\xFD",\n "asks": [{"input": "nguoi", "label": "Ng\u01B0\u1EDDi x\u1EED l\xFD", "type": "member", "required": true}],\n "set": {"xu_ly": {"input": "nguoi"}}\n },\n {\n "type": "Act",\n "key": "chuyen_du_an",\n "label": "Chuy\u1EC3n d\u1EF1 \xE1n",\n "asks": [\n {\n "input": "du_an_moi",\n "label": "Sang d\u1EF1 \xE1n",\n "type": "link",\n "entity": "du_an",\n "required": true,\n "add": ["ten_du_an", "chu_dau_tu", "so_can"]\n }\n ],\n "set": {"du_an": {"input": "du_an_moi"}},\n "confirm": true\n }\n ]\n },\n {\n "type": "Section",\n "key": "bo_ho_so",\n "title": "B\u1ED9 h\u1ED3 s\u01A1",\n "at": ["ngay_nhan_giay_to", "ngay_lap_ho_so"],\n "children": [\n "ngay_nhan_giay_to",\n "ngay_lap_ho_so",\n {\n "type": "Act",\n "key": "tao_giay_to",\n "label": "T\u1EA1o gi\u1EA5y t\u1EDD",\n "templates": [\n {"template": "don_dang_ky"},\n {"template": "mau_02_chua_co_nha", "when": {"tinh_trang_nha_o": ["chua_co_nha"]}},\n {"template": "mau_03_nha_chat", "when": {"tinh_trang_nha_o": ["nha_chat"]}},\n {"template": "mau_05_thu_nhap", "when": {"doi_tuong": ["thu_nhap_thap"]}}\n ],\n "into": "bo_ho_so",\n "when": {"giai_doan": ["ngay_nhan_giay_to", "ngay_lap_ho_so"]}\n },\n {\n "type": "Act",\n "key": "nop_cdt",\n "label": "N\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "when": {"dich_vu": ["tron_goi"], "giai_doan": ["ngay_lap_ho_so"]},\n "requires": ["bo_ho_so", "loai_can"],\n "asks": ["loai_can"],\n "set": {"giai_doan": "ngay_nop_cdt"},\n "confirm": true\n },\n {\n "type": "Act",\n "key": "to_ngay_nhan_giay_to",\n "label": "\u0110\xE3 nh\u1EADn gi\u1EA5y t\u1EDD",\n "when": {"giai_doan": {"empty": true}, "dich_vu": ["tron_goi"]},\n "asks": [{"field": "ngay_nhan_giay_to", "starts": "today"}],\n "set": {"giai_doan": "ngay_nhan_giay_to"}\n },\n {\n "type": "Act",\n "key": "to_ngay_lap_ho_so",\n "label": "\u0110\xE3 l\u1EADp h\u1ED3 s\u01A1",\n "when": {"giai_doan": ["ngay_nhan_giay_to"], "dich_vu": ["tron_goi"]},\n "asks": [{"field": "ngay_lap_ho_so", "starts": "today"}],\n "set": {"giai_doan": "ngay_lap_ho_so"}\n },\n {\n "type": "Act",\n "key": "to_ngay_ban_giao",\n "label": "\u0110\xE3 b\xE0n giao h\u1ED3 s\u01A1",\n "when": {"giai_doan": ["ngay_nhan_giay_to"], "dich_vu": ["lam_ho_so"]},\n "asks": [{"field": "ngay_ban_giao", "starts": "today"}],\n "set": {"giai_doan": "ngay_ban_giao"}\n }\n ]\n }\n ]\n },\n {\n "type": "Act",\n "key": "in_danh_sach",\n "label": "In danh s\xE1ch n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "on": "view",\n "template": "danh_sach_nop"\n }\n ]\n }\n ]\n }\n}\n```\n\n## Calls\n\n**The job.** Call a lead from their record, and keep what was said as a touch on its timeline.\n\n**Read.** An Act\'s `record` (into the `timeline` child: audio, transcript, when and who) and `fills` (the touch\'s fields an agent fills from the transcript).\n\n**Keys.** `model/act-node`, `model/layouts`\n\n**Not when.** Calls made and logged elsewhere \u2014 a `timeline` with its own add keeps the note.\n\n```json\n{\n "tables": [\n {\n "ref": "khach_tiem_nang",\n "title": "ho_ten",\n "fields": {\n "ho_ten": {"type": "text"},\n "ten_cong_ty": {"type": "text"},\n "vai_tro": {\n "type": "select",\n "options": [\n {"alias": "chu_doanh_nghiep", "color": "emerald"},\n {"alias": "ke_toan_truong", "color": "blue"},\n {"alias": "quan_ly", "color": "teal"},\n {"alias": "nhan_vien", "color": "sky"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n "loai_hinh": {\n "type": "select",\n "options": [\n {"alias": "thuong_mai", "color": "blue"},\n {"alias": "san_xuat", "color": "amber"},\n {"alias": "dich_vu", "color": "violet"},\n {"alias": "xay_dung", "color": "orange"},\n {"alias": "ho_kinh_doanh", "color": "teal"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n "quy_mo": {\n "type": "select",\n "options": [\n {"alias": "q_1_10", "color": "sky"},\n {"alias": "q_11_50", "color": "blue"},\n {"alias": "q_51_200", "color": "indigo"},\n {"alias": "q_200", "color": "violet"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n "so_dien_thoai": {"type": "text"},\n "zalo": {"type": "text"},\n "email": {"type": "text"},\n "trang_ca_nhan": {"type": "text", "format": "link"},\n "anh": {"type": "files"},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "moi", "color": "gray"},\n {"alias": "du_dieu_kien", "color": "blue"},\n {"alias": "da_lien_he", "color": "amber"},\n {"alias": "da_chuyen_doi", "color": "emerald"},\n {"alias": "loai", "color": "red"}\n ]\n },\n "ly_do_loai": {\n "type": "select",\n "options": [\n {"alias": "khong_phu_hop", "color": "orange"},\n {"alias": "qua_nho", "color": "amber"},\n {"alias": "chua_co_ngan_sach", "color": "yellow"},\n {"alias": "sai_nguoi", "color": "blue"},\n {"alias": "yeu_cau_dung", "color": "red"},\n {"alias": "trung", "color": "zinc"}\n ]\n },\n "vi_sao_du_dieu_kien": {"type": "text"},\n "phu_trach": {"type": "member"},\n "nguon": {"type": "link", "table": "kenh", "multi": false},\n "lien_he": {"type": "link", "table": "lien_he", "multi": false},\n "cong_ty": {"type": "link", "table": "cong_ty", "multi": false},\n "ngay_chuyen_doi": {"type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n "ghi_chu": {"type": "text", "format": "markdown"},\n "tin_hieu": {"type": "link", "table": "tin_hieu", "multi": true},\n "buoc_tiep": {"type": "link", "table": "buoc_tiep", "multi": true},\n "tuong_tac": {"type": "link", "table": "tuong_tac", "multi": true},\n "do_nong": {\n "type": "formula",\n "options": [\n {"alias": "nong", "label": "N\xF3ng", "color": "orange"},\n {"alias": "nguoi", "label": "Ngu\u1ED9i", "color": "sky"}\n ]\n },\n "han_buoc_tiep": {"type": "rollup"},\n "khong_lien_he": {"type": "formula"}\n },\n "records": {\n "subtitle": ["ten_cong_ty"],\n "image": "anh",\n "party": "person",\n "due": ["han_buoc_tiep"],\n "status": {\n "field": "trang_thai",\n "closed": ["da_chuyen_doi", "loai"],\n "dates": {"da_chuyen_doi": "ngay_chuyen_doi"}\n }\n }\n },\n {\n "ref": "tin_hieu",\n "title": "tom_tat",\n "fields": {\n "tom_tat": {"type": "text"},\n "noi_dung": {"type": "text", "format": "markdown"},\n "anh_chup": {"type": "files"},\n "lien_ket": {"type": "text", "format": "link"},\n "kenh": {"type": "link", "table": "kenh", "multi": false},\n "loai": {\n "type": "select",\n "options": [\n {"alias": "hoi_dich_vu", "color": "emerald"},\n {"alias": "than_phien", "color": "blue"},\n {"alias": "tuyen_ke_toan", "color": "amber"},\n {"alias": "moi_thanh_lap", "color": "violet"},\n {"alias": "gioi_thieu", "color": "teal"},\n {"alias": "danh_ba", "color": "zinc"},\n {"alias": "nhieu", "color": "stone"}\n ]\n },\n "ngay_dang": {"type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n "ngay_xay_ra": {"type": "formula"}\n },\n "records": {"subtitle": ["kenh", "ngay_xay_ra"], "image": "anh_chup"}\n },\n {\n "ref": "buoc_tiep",\n "title": "vi_sao_luc_nay",\n "fields": {\n "vi_sao_luc_nay": {"type": "text"},\n "viec": {\n "type": "select",\n "options": [\n {"alias": "binh_luan", "color": "blue", "mark": {"kind": "icon", "name": "message-square"}},\n {"alias": "nhan_facebook", "color": "violet", "mark": {"kind": "brand", "name": "facebook"}},\n {"alias": "nhan_zalo", "color": "sky", "mark": {"kind": "brand", "name": "zalo"}},\n {"alias": "goi_dien", "color": "teal", "mark": {"kind": "icon", "name": "phone"}},\n {"alias": "gui_email", "color": "indigo", "mark": {"kind": "icon", "name": "mail"}},\n {"alias": "gui_bang_gia", "color": "amber", "mark": {"kind": "icon", "name": "receipt"}}\n ]\n },\n "ban_nhap": {"type": "text", "format": "markdown"},\n "han": {"type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n "tinh_trang": {\n "type": "select",\n "options": [\n {"alias": "cho", "color": "blue"},\n {"alias": "xong", "color": "emerald"},\n {"alias": "bo_qua", "color": "zinc"}\n ]\n }\n },\n "records": {\n "subtitle": ["viec"],\n "mark": "viec",\n "due": ["han"],\n "status": {"field": "tinh_trang", "closed": ["xong", "bo_qua"]}\n }\n },\n {\n "ref": "tuong_tac",\n "title": "tom_tat",\n "fields": {\n "tom_tat": {"type": "text"},\n "thoi_diem": {"type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n "qua": {\n "type": "select",\n "options": [\n {"alias": "goi_dien", "color": "teal", "mark": {"kind": "icon", "name": "phone"}},\n {"alias": "zalo", "color": "sky", "mark": {"kind": "brand", "name": "zalo"}},\n {"alias": "facebook", "color": "blue", "mark": {"kind": "brand", "name": "facebook"}},\n {"alias": "email", "color": "indigo", "mark": {"kind": "icon", "name": "mail"}},\n {"alias": "gap_truc_tiep", "color": "emerald", "mark": {"kind": "icon", "name": "users"}},\n {"alias": "khac", "color": "zinc", "mark": {"kind": "icon", "name": "ellipsis"}}\n ]\n },\n "phan_hoi": {\n "type": "select",\n "options": [\n {"alias": "dang_cho", "color": "blue"},\n {"alias": "da_tra_loi", "color": "green"},\n {"alias": "quan_tam", "color": "amber"},\n {"alias": "khong_phu_hop", "color": "red"},\n {"alias": "khong_phan_hoi", "color": "zinc"},\n {"alias": "sai_so", "color": "stone"}\n ]\n },\n "ghi_am": {"type": "files"},\n "nguyen_van": {"type": "text", "format": "markdown"},\n "hen_lai": {"type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n "nguoi_thuc_hien": {"type": "member"}\n },\n "records": {"subtitle": ["qua", "phan_hoi"]}\n }\n ],\n "body": {\n "type": "App",\n "key": "khach_tiem_nang",\n "name": "Kh\xE1ch ti\u1EC1m n\u0103ng",\n "icon": "radar",\n "color": "violet",\n "children": [\n {\n "type": "List",\n "entity": "khach_tiem_nang",\n "columns": ["phu_trach", "han_buoc_tiep"],\n "filters": ["phu_trach"],\n "create": [\n "ho_ten",\n "trang_ca_nhan",\n "so_dien_thoai",\n "ten_cong_ty",\n {"field": "nguon", "add": ["ten", "loai"]}\n ],\n "children": [\n {"type": "Reading", "key": "register_reading_0", "by": "phu_trach"},\n {\n "type": "Reading",\n "key": "register_reading_1",\n "label": "Kh\xE1ch chuy\u1EC3n \u0111\u1ED5i",\n "by": "ngay_chuyen_doi"\n },\n {"type": "Check", "field": "khong_lien_he", "resolve": "da_lien_he"},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "danh_gia",\n "title": "\u0110\xE1nh gi\xE1",\n "at": ["moi"],\n "children": [\n "vai_tro",\n "loai_hinh",\n "quy_mo",\n "vi_sao_du_dieu_kien",\n "do_nong",\n {\n "type": "List",\n "entity": "tin_hieu",\n "label": "T\xEDn hi\u1EC7u",\n "columns": ["loai", "noi_dung"],\n "create": ["tom_tat", "anh_chup", "loai", "noi_dung", "kenh", "lien_ket", "ngay_dang"]\n },\n {\n "type": "Act",\n "key": "danh_gia_dat",\n "label": "\u0110\u1EE7 \u0111i\u1EC1u ki\u1EC7n",\n "when": {"trang_thai": ["moi"]},\n "requires": ["vi_sao_du_dieu_kien"],\n "set": {"trang_thai": "du_dieu_kien"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "lien_he",\n "title": "Li\xEAn h\u1EC7",\n "at": ["du_dieu_kien"],\n "children": [\n "so_dien_thoai",\n "phu_trach",\n "trang_ca_nhan",\n "zalo",\n "email",\n {"field": "ghi_chu", "block": false},\n {\n "type": "List",\n "entity": "buoc_tiep",\n "label": "B\u01B0\u1EDBc ti\u1EBFp theo",\n "columns": ["ban_nhap"],\n "create": ["vi_sao_luc_nay", "viec", "han", "ban_nhap"],\n "children": [\n {\n "type": "Act",\n "key": "lam_xong",\n "label": "\u0110\xE3 l\xE0m",\n "when": {"tinh_trang": ["cho"]},\n "set": {"tinh_trang": "xong"}\n }\n ]\n },\n {\n "type": "List",\n "entity": "tuong_tac",\n "label": "T\u01B0\u01A1ng t\xE1c",\n "layout": "timeline",\n "create": ["tom_tat", "qua", "phan_hoi", "thoi_diem", "hen_lai"]\n },\n {\n "type": "Act",\n "key": "ghi_cuoc_goi",\n "label": "Ghi \xE2m cu\u1ED9c g\u1ECDi",\n "when": {"trang_thai": ["du_dieu_kien"]},\n "fills": ["tom_tat", "phan_hoi"],\n "record": {\n "into": "tuong_tac",\n "audio": "ghi_am",\n "transcript": "nguyen_van",\n "at": "thoi_diem",\n "by": "nguoi_thuc_hien"\n }\n },\n {\n "type": "Act",\n "key": "da_lien_he",\n "label": "\u0110\xE1nh d\u1EA5u \u0111\xE3 li\xEAn h\u1EC7",\n "when": {"trang_thai": ["du_dieu_kien"]},\n "set": {"trang_thai": "da_lien_he"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "chuyen_doi",\n "title": "Chuy\u1EC3n \u0111\u1ED5i",\n "at": ["da_lien_he"],\n "children": [\n "nguon",\n {"field": "lien_he", "edit": false},\n {"field": "cong_ty", "edit": false},\n {\n "type": "Act",\n "key": "chuyen_doi",\n "label": "Chuy\u1EC3n \u0111\u1ED5i",\n "when": {"trang_thai": ["da_lien_he"]},\n "workflow": "chuyen_doi_khach",\n "confirm": true\n }\n ]\n },\n {\n "type": "Section",\n "key": "loai",\n "title": "Lo\u1EA1i",\n "at": ["loai"],\n "children": [\n "ly_do_loai",\n {\n "type": "Act",\n "key": "loai",\n "label": "Lo\u1EA1i",\n "when": {"trang_thai": ["moi", "du_dieu_kien", "da_lien_he"]},\n "asks": ["ly_do_loai"],\n "set": {"trang_thai": "loai"},\n "danger": true\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Approval\n\n**The job.** Raise a payment request, see it through approval, and read what is waiting at the top of the list.\n\n**Read.** `records.de_nghi.status` with its `history` (each move: when and who), Sections staged by `at`, and the List\'s Readings leading with the headline number. The history stays because the business reads who signed each move; where only the day a stage was reached matters, the status stamps a date on the row instead (the Gantt above).\n\n**Keys.** `model/status`, `model/section-node`, `model/reading-node`\n\n**Not when.** Rows that never move \u2014 a reference list (a price list, a catalog) has no status and no headline number.\n\n```json\n{\n "tables": [\n {\n "ref": "de_nghi",\n "title": "noi_dung",\n "fields": {\n "noi_dung": {"type": "text"},\n "so_de_nghi": {"type": "text"},\n "loai": {\n "type": "select",\n "options": [\n {"alias": "thanh_toan_ncc", "color": "violet"},\n {"alias": "nhan_cong", "color": "indigo"},\n {"alias": "chi_phi_cong_truong", "color": "sky"},\n {"alias": "tam_ung", "color": "amber"},\n {"alias": "hoan_ung", "color": "teal"}\n ]\n },\n "du_an": {"type": "link", "table": "du_an", "multi": false},\n "nha_cung_cap": {"type": "link", "table": "nha_cung_cap", "multi": false},\n "so_tien": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "han_thanh_toan": {"type": "date"},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "nhap", "color": "slate"},\n {"alias": "cho_duyet", "color": "amber"},\n {"alias": "cho_chu_tich", "color": "violet"},\n {"alias": "da_duyet", "color": "blue"},\n {"alias": "da_chi", "color": "green"},\n {"alias": "tu_choi", "color": "rose"}\n ]\n },\n "nguoi_de_nghi": {"type": "member"},\n "chung_tu": {"type": "files"},\n "nguoi_duyet": {"type": "member"},\n "duyet_luc": {"type": "date", "format": "datetime"},\n "ly_do_tu_choi": {"type": "text"},\n "phieu_chi": {"type": "files"},\n "ngan_sach_du_an": {"type": "lookup"},\n "da_duyet_du_an": {"type": "lookup"},\n "vuot_ngan_sach": {"type": "formula"},\n "sat_ngan_sach": {"type": "formula"},\n "da_gui": {"type": "formula"},\n "nhap_at": {"type": "date", "format": "datetime"},\n "cho_duyet_at": {"type": "date", "format": "datetime"},\n "cho_chu_tich_at": {"type": "date", "format": "datetime"},\n "da_duyet_at": {"type": "date", "format": "datetime"}\n },\n "records": {\n "subtitle": ["du_an"],\n "figure": "so_tien",\n "limits": {"da_duyet_du_an": "ngan_sach_du_an"},\n "due": ["han_thanh_toan"],\n "starts": {"nguoi_de_nghi": "me"},\n "status": {\n "field": "trang_thai",\n "closed": ["da_chi", "tu_choi"],\n "dropped": ["tu_choi"],\n "dates": {\n "nhap": "nhap_at",\n "cho_duyet": "cho_duyet_at",\n "cho_chu_tich": "cho_chu_tich_at",\n "da_duyet": "da_duyet_at"\n },\n "history": "lich_su_duyet",\n "stages": {\n "nhap": {"owner": "nguoi_de_nghi"},\n "cho_duyet": {"owner": {"group": "<id of the giam_doc group>"}, "within": 1},\n "cho_chu_tich": {\n "owner": {"group": "<id of the chu_tich group>"},\n "within": 2,\n "when": {"so_tien": {"gt": 200000000}}\n },\n "da_duyet": {"owner": {"group": "<id of the ke_toan group>"}, "within": 2}\n }\n }\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "de_nghi_chi",\n "name": "\u0110\u1EC1 ngh\u1ECB thanh to\xE1n",\n "icon": "hand-coins",\n "color": "amber",\n "children": [\n {\n "type": "List",\n "entity": "de_nghi",\n "row": {"subtitle": [{"field": "du_an", "add": ["ten", "ma_du_an", "chu_dau_tu", "ngan_sach"]}]},\n "columns": ["nguoi_de_nghi"],\n "filters": ["nguoi_de_nghi", "du_an"],\n "group": "du_an",\n "create": [\n "noi_dung",\n "loai",\n "du_an",\n {"field": "nha_cung_cap", "add": true},\n "so_tien",\n "han_thanh_toan",\n "nguoi_de_nghi"\n ],\n "children": [\n {\n "type": "Reading",\n "key": "register_reading_0",\n "label": "\u0110\xE3 duy\u1EC7t, ch\u1EDD chi",\n "value": "so_tien",\n "where": {"trang_thai": ["da_duyet"]}\n },\n {"type": "Reading", "key": "register_reading_1", "value": "so_tien", "by": "han_thanh_toan"},\n {"type": "Check", "field": "da_gui", "on": ["update"]},\n {"type": "Check", "field": "vuot_ngan_sach"},\n {"type": "Check", "field": "sat_ngan_sach"},\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {\n "type": "Section",\n "key": "de_nghi",\n "title": "\u0110\u1EC1 ngh\u1ECB",\n "at": ["nhap"],\n "children": [\n "so_de_nghi",\n "loai",\n "nha_cung_cap",\n "han_thanh_toan",\n "nguoi_de_nghi",\n {"field": "chung_tu", "label": "Ch\u1EE9ng t\u1EEB k\xE8m"},\n {\n "type": "Act",\n "key": "gui_duyet",\n "label": "G\u1EEDi duy\u1EC7t",\n "when": {"trang_thai": ["nhap"]},\n "requires": ["chung_tu", "nguoi_de_nghi"],\n "set": {"trang_thai": "cho_duyet"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "duyet",\n "title": "Duy\u1EC7t",\n "at": ["cho_duyet", "cho_chu_tich"],\n "children": [\n {"field": "nguoi_duyet", "edit": false},\n {"field": "duyet_luc", "edit": false},\n {"field": "ly_do_tu_choi", "edit": false},\n {\n "type": "Act",\n "key": "rut_lai",\n "label": "R\xFAt l\u1EA1i",\n "when": {"trang_thai": ["cho_duyet", "cho_chu_tich"]},\n "set": {"trang_thai": "nhap"},\n "confirm": true\n }\n ]\n },\n {\n "type": "Section",\n "key": "da_chi",\n "title": "\u0110\xE3 chi",\n "at": ["da_chi"],\n "children": [{"field": "phieu_chi", "label": "Phi\u1EBFu chi", "edit": false}]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Dashboard\n\n**The job.** The owner\'s question \u2014 what came in, what went out, where it went, and what is still unmatched \u2014 answered over one period.\n\n**Read.** An App of Readings and no List: Readings with `over` (the period windows them), a Reading `by` the day, Readings `by` a field, and a List of rows to act on that opens in its `app`.\n\n**Keys.** `model/app-node`, `model/reading-node`\n\n**Not when.** One job\'s headline numbers \u2014 those lead that job\'s List as its Readings.\n\n```json\n{\n "tables": [\n {\n "ref": "giao_dich",\n "title": "noi_dung",\n "fields": {\n "noi_dung": {"type": "text"},\n "ngay": {"type": "date"},\n "loai": {"type": "select", "options": [{"alias": "thu", "color": "green"}, {"alias": "chi", "color": "rose"}]},\n "so_tien": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "bien_dong": {"type": "formula", "notation": {"style": "currency", "currency": "VND"}},\n "doi_tuong": {"type": "link", "table": "doi_tuong", "multi": false},\n "danh_muc": {\n "type": "select",\n "options": [\n {"alias": "doanh_thu", "color": "green"},\n {"alias": "hang_hoa", "color": "blue"},\n {"alias": "van_chuyen", "color": "indigo"},\n {"alias": "nhan_su", "color": "violet"},\n {"alias": "van_phong", "color": "sky"},\n {"alias": "thue_phi", "color": "slate"},\n {"alias": "ngan_hang", "color": "teal"},\n {"alias": "khac", "color": "gray"}\n ]\n },\n "sao_ke": {"type": "link", "table": "sao_ke", "multi": false},\n "hoa_don": {"type": "link", "table": "hoa_don", "multi": true},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "chua_khop", "color": "amber"},\n {"alias": "da_khop", "color": "green"},\n {"alias": "khong_can_hd", "color": "slate"}\n ]\n },\n "khop_luc": {"type": "date", "format": "datetime"}\n },\n "records": {\n "subtitle": ["ngay"],\n "figure": "bien_dong",\n "status": {"field": "trang_thai", "closed": ["da_khop", "khong_can_hd"], "dates": {"da_khop": "khop_luc"}}\n }\n },\n {\n "ref": "hoa_don",\n "title": "doi_tuong",\n "fields": {\n "doi_tuong": {"type": "link", "table": "doi_tuong", "multi": false},\n "so_hoa_don": {"type": "text"},\n "ngay_hoa_don": {"type": "date"},\n "so_ngay_qua_han": {"type": "formula", "notation": {"style": "unit", "unit": "day"}},\n "chieu": {\n "type": "select",\n "options": [{"alias": "ban_ra", "color": "green"}, {"alias": "mua_vao", "color": "violet"}]\n },\n "tong_cong": {"type": "formula", "notation": {"style": "currency", "currency": "VND"}},\n "thue_suat": {"type": "formula", "notation": {"style": "unit", "unit": "percent"}},\n "tep": {"type": "files"},\n "giao_dich": {"type": "link", "table": "giao_dich", "multi": true}\n },\n "records": {"subtitle": ["so_hoa_don", "ngay_hoa_don"], "image": "tep", "figure": "tong_cong"}\n }\n ],\n "body": {\n "type": "App",\n "key": "dong_tien",\n "name": "D\xF2ng ti\u1EC1n",\n "icon": "chart-line",\n "color": "emerald",\n "children": [\n {\n "type": "Reading",\n "key": "dashboard_reading_0",\n "label": "Ti\u1EC1n v\xE0o",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "over": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_1",\n "label": "Ti\u1EC1n ra",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "over": "ngay",\n "better": "down"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_2",\n "label": "D\xF2ng ti\u1EC1n r\xF2ng",\n "entity": "giao_dich",\n "value": "bien_dong",\n "by": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_3",\n "label": "Chi theo danh m\u1EE5c",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "by": "danh_muc",\n "over": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_4",\n "label": "Thu theo kh\xE1ch h\xE0ng",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "by": "doi_tuong",\n "over": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_5",\n "label": "C\xF4ng n\u1EE3 b\xE1n ra theo tu\u1ED5i n\u1EE3",\n "entity": "hoa_don",\n "value": "tong_cong",\n "where": {"chieu": ["ban_ra"]},\n "by": "doi_tuong",\n "across": {"field": "so_ngay_qua_han", "tiers": [1, 8, 16, 31]}\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_6",\n "label": "S\u1ED1 ng\xE0y qu\xE1 h\u1EA1n b\xECnh qu\xE2n",\n "entity": "hoa_don",\n "value": {"avg": "so_ngay_qua_han"},\n "where": {"chieu": ["ban_ra"]}\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_7",\n "label": "Thu\u1EBF su\u1EA5t b\xECnh qu\xE2n",\n "entity": "hoa_don",\n "value": {"avg": "thue_suat"}\n },\n {\n "type": "List",\n "key": "dashboard_reading_8",\n "entity": "giao_dich",\n "where": {"trang_thai": ["chua_khop"]},\n "app": "doi_chieu",\n "label": "D\xF2ng ch\u01B0a kh\u1EDBp",\n "columns": ["sao_ke"]\n }\n ]\n }\n}\n```\n';
1768
+ var examples_default = '# Worked examples\n\nEach section is one treatment, cut from a complete workspace of an invented business: the job, the keys to\nread, where the treatment is wrong, and the excerpt \u2014 the app\'s tree, the App node `set_app` takes as `body`,\nwhich the check passes with no finding; the tables it draws as the items of a `set_tables` call, each with its\n`title` and `records`; those tables\' fields it names, by what each holds and how it is drawn; and the templates it makes.\nLeft out, for the page\'s size: a Section that carries nothing of the treatment, every `label` and `description` of a table or field, `required`, `unique`, `default`, a link\'s\n`pair`, how a computed field computes, the `tables` and `templates` maps an App adds (ids from the\n`set_tables` answer), and template contents \u2014 a call states each table\'s and field\'s `label`. Choosing\namong them: the `design` reference. Every key: the `model` reference, at the pages each section names under\n**Keys**.\n\n## Calendar\n\n**The job.** Book visits on the days they happen, and see a week\'s work at a glance.\n\n**Read.** `layout: "calendar"` on the List; each visit stands over its span because its `records` line holds two dates (`scheduled_for`, `ends_at`).\n\n**Keys.** `reference/calendar`\n\n**Not when.** Rows booked on a resource that two may not share at once \u2014 that is `lanes`. Rows with a date nobody plans by (an invoice\'s date) stay a table.\n\n```json\n{\n "tables": [\n {\n "ref": "service_visit",\n "title": "customer",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "scheduled_for": {"type": "date", "format": "datetime"},\n "ends_at": {"type": "date", "format": "datetime"},\n "engineer": {"type": "link", "table": "engineer", "multi": false},\n "job": {\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n "stage": {\n "type": "select",\n "options": [\n {"alias": "booked_on", "color": "blue"},\n {"alias": "parts_ready_on", "color": "blue"},\n {"alias": "arrived_on", "color": "blue"},\n {"alias": "completed_on", "color": "green"}\n ]\n },\n "booked_on": {"type": "date"},\n "parts_ready_on": {"type": "date"},\n "arrived_on": {"type": "date"},\n "hours": {"type": "number", "notation": {"style": "unit", "unit": "h"}},\n "report": {"type": "text", "format": "markdown"},\n "completed_on": {"type": "date"}\n },\n "records": {\n "subtitle": ["scheduled_for", "ends_at"],\n "figure": "hours",\n "due": ["scheduled_for"],\n "status": {\n "field": "stage",\n "closed": ["completed_on"],\n "dates": {\n "booked_on": "booked_on",\n "parts_ready_on": "parts_ready_on",\n "arrived_on": "arrived_on",\n "completed_on": "completed_on"\n }\n }\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "visits",\n "name": "Service visits",\n "icon": "calendar",\n "color": "emerald",\n "children": [\n {\n "type": "List",\n "entity": "service_visit",\n "row": {"title": {"add": ["name", "credit_limit"]}},\n "columns": ["engineer", "job"],\n "filters": ["customer"],\n "sort": {"field": "scheduled_for"},\n "layout": "calendar",\n "create": [\n "customer",\n "job",\n "scheduled_for",\n "ends_at",\n {"field": "engineer", "add": ["name", {"field": "base", "add": ["name", "city"]}]}\n ],\n "children": [\n {"type": "Reading", "key": "register_reading_0", "label": "Repairs", "where": {"job": ["repair"]}},\n {"type": "Reading", "key": "register_reading_1", "value": "hours", "by": "engineer"},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "visit",\n "title": "Visit",\n "at": ["booked_on", "parts_ready_on"],\n "children": [\n "job",\n "booked_on",\n "parts_ready_on",\n {\n "type": "Act",\n "key": "reschedule",\n "label": "Reschedule",\n "asks": ["scheduled_for", "ends_at"],\n "when": {"stage": ["booked_on", "parts_ready_on"]}\n },\n {\n "type": "Act",\n "key": "to_booked_on",\n "label": "Booked",\n "when": {"stage": {"empty": true}},\n "asks": [{"field": "booked_on", "starts": "today"}],\n "set": {"stage": "booked_on"}\n },\n {\n "type": "Act",\n "key": "to_parts_ready_on",\n "label": "Parts ready",\n "when": {"stage": ["booked_on"], "job": ["repair"]},\n "asks": [{"field": "parts_ready_on", "starts": "today"}],\n "set": {"stage": "parts_ready_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on",\n "label": "On site",\n "when": {"stage": ["booked_on"], "job": ["inspection"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on_2",\n "label": "On site",\n "when": {"stage": ["parts_ready_on"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "report",\n "title": "Report",\n "at": ["arrived_on"],\n "children": [\n "engineer",\n "arrived_on",\n {"field": "report", "block": false},\n {\n "type": "Act",\n "key": "complete",\n "label": "Complete visit",\n "requires": ["engineer"],\n "asks": [\n "hours",\n {"input": "summary", "label": "What was done", "type": "long_text", "required": true}\n ],\n "set": {"report": {"input": "summary"}, "stage": "completed_on"},\n "when": {"stage": ["arrived_on"]}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Lanes\n\n**The job.** Put each service visit on an engineer\'s day by the hour, and see at once who is free between visits.\n\n**Read.** `layout` `lanes` with `by` naming the one-row link to the resource (the engineer); every engineer stands as a lane, empty or not.\n\n**Keys.** `reference/lanes`\n\n**Not when.** The resource is not a row of the model (a free-text room name), or rows never compete for one \u2014 a `calendar` reads them by day.\n\n```json\n{\n "tables": [\n {\n "ref": "service_visit",\n "title": "customer",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "scheduled_for": {"type": "date", "format": "datetime"},\n "ends_at": {"type": "date", "format": "datetime"},\n "engineer": {"type": "link", "table": "engineer", "multi": false},\n "job": {\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n "stage": {\n "type": "select",\n "options": [\n {"alias": "booked_on", "color": "blue"},\n {"alias": "parts_ready_on", "color": "blue"},\n {"alias": "arrived_on", "color": "blue"},\n {"alias": "completed_on", "color": "green"}\n ]\n },\n "booked_on": {"type": "date"},\n "parts_ready_on": {"type": "date"},\n "arrived_on": {"type": "date"},\n "hours": {"type": "number", "notation": {"style": "unit", "unit": "h"}},\n "completed_on": {"type": "date"}\n },\n "records": {\n "subtitle": ["scheduled_for", "ends_at"],\n "figure": "hours",\n "due": ["scheduled_for"],\n "status": {\n "field": "stage",\n "closed": ["completed_on"],\n "dates": {\n "booked_on": "booked_on",\n "parts_ready_on": "parts_ready_on",\n "arrived_on": "arrived_on",\n "completed_on": "completed_on"\n }\n }\n }\n },\n {\n "ref": "engineer",\n "title": "name",\n "fields": {"name": {"type": "text"}, "base": {"type": "link", "table": "warehouse", "multi": false}},\n "records": {"subtitle": ["base"], "party": "person"}\n }\n ],\n "body": {\n "type": "App",\n "key": "dispatch",\n "name": "Dispatch",\n "icon": "wrench",\n "color": "sky",\n "children": [\n {\n "type": "List",\n "entity": "service_visit",\n "row": {\n "title": {"add": ["name", "credit_limit"]},\n "subtitle": [{"field": "scheduled_for", "edit": false}, {"field": "ends_at", "edit": false}]\n },\n "columns": ["job"],\n "search": ["customer"],\n "layout": {"lanes": {"by": "engineer"}},\n "create": [\n {"field": "engineer", "add": ["name", {"field": "base", "add": ["name", "city"]}]},\n "scheduled_for",\n "ends_at",\n "customer",\n "job"\n ],\n "children": [\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {\n "type": "Section",\n "key": "booking",\n "title": "Booking",\n "children": [\n "engineer",\n "job",\n {\n "type": "Act",\n "key": "to_booked_on",\n "label": "Booked",\n "when": {"stage": {"empty": true}},\n "asks": [{"field": "booked_on", "starts": "today"}],\n "set": {"stage": "booked_on"}\n },\n {\n "type": "Act",\n "key": "to_parts_ready_on",\n "label": "Parts ready",\n "when": {"stage": ["booked_on"], "job": ["repair"]},\n "asks": [{"field": "parts_ready_on", "starts": "today"}],\n "set": {"stage": "parts_ready_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on",\n "label": "On site",\n "when": {"stage": ["booked_on"], "job": ["inspection"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n },\n {\n "type": "Act",\n "key": "to_arrived_on_2",\n "label": "On site",\n "when": {"stage": ["parts_ready_on"]},\n "asks": [{"field": "arrived_on", "starts": "today"}],\n "set": {"stage": "arrived_on"}\n },\n {\n "type": "Act",\n "key": "to_completed_on",\n "label": "Completed",\n "when": {"stage": ["arrived_on"]},\n "asks": [{"field": "completed_on", "starts": "today"}],\n "set": {"stage": "completed_on"}\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Gantt\n\n**The job.** Read every hire as a bar from the day the machine goes out to the day it is due back, against today.\n\n**Read.** `layout` `gantt` with its `start`; with no end stated, the bar ends at the entity\'s first `due`. The day a hire comes back (`ngay_tra`) is stamped by the status\'s `dates`, so no act sets it.\n\n**Keys.** `reference/gantt`\n\n**Not when.** Rows that last a day or less (a visit, a shift) \u2014 `calendar` or `lanes`; a plan with no dates set yet.\n\n```json\n{\n "tables": [\n {\n "ref": "phieu_thue",\n "title": "khach_hang",\n "fields": {\n "khach_hang": {"type": "link", "table": "khach_hang", "multi": false},\n "so_phieu": {"type": "text"},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "moi", "color": "slate"},\n {"alias": "dang_thue", "color": "blue"},\n {"alias": "da_tra", "color": "green"},\n {"alias": "huy", "color": "gray"}\n ]\n },\n "phu_trach": {"type": "member"},\n "ngay_giao": {"type": "date"},\n "han_tra": {"type": "date"},\n "ngay_tra": {"type": "date"},\n "tinh_trang_tra": {\n "type": "select",\n "options": [\n {"alias": "tot", "color": "green"},\n {"alias": "tray_xuoc", "color": "amber"},\n {"alias": "hu_hong", "color": "red"}\n ]\n },\n "so_ngay": {"type": "formula"},\n "tong_tien": {"type": "rollup"}\n },\n "records": {\n "subtitle": ["so_phieu"],\n "figure": "tong_tien",\n "due": ["han_tra"],\n "status": {"field": "trang_thai", "closed": ["da_tra", "huy"], "dates": {"da_tra": "ngay_tra"}}\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "tien_do_thue",\n "name": "Ti\u1EBFn \u0111\u1ED9 cho thu\xEA",\n "icon": "calendar-range",\n "color": "teal",\n "children": [\n {\n "type": "List",\n "entity": "phieu_thue",\n "row": {"title": {"add": true}},\n "columns": ["phu_trach"],\n "filters": ["phu_trach", "khach_hang"],\n "layout": {"gantt": {"start": "ngay_giao"}},\n "create": false,\n "remove": false,\n "children": [\n {\n "type": "Reading",\n "key": "register_reading_0",\n "label": "Phi\u1EBFu \u0111ang cho thu\xEA",\n "where": {"trang_thai": ["dang_thue"]}\n },\n {\n "type": "Reading",\n "key": "register_reading_1",\n "label": "Ti\u1EC1n thu\xEA \u0111ang ch\u1EA1y",\n "value": "tong_tien",\n "where": {"trang_thai": ["dang_thue"]}\n },\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "phieu_thue",\n "title": "Phi\u1EBFu thu\xEA",\n "children": ["ngay_giao", "han_tra"]\n },\n {\n "type": "Section",\n "key": "dang_thue",\n "title": "\u0110ang thu\xEA",\n "at": ["dang_thue"],\n "children": [\n "so_ngay",\n "ngay_tra",\n {\n "type": "Act",\n "key": "nhan_tra",\n "label": "Nh\u1EADn m\xE1y tr\u1EA3",\n "when": {"trang_thai": ["dang_thue"]},\n "asks": ["tinh_trang_tra"],\n "set": {"trang_thai": "da_tra"},\n "confirm": true\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Roster\n\n**The job.** See who works which shift on each day of the week or the month.\n\n**Read.** `layout: "roster"` over the people; the child List (`lich_truc`) is the rows that fill the days \u2014 one link back and a date on its line.\n\n**Keys.** `reference/roster`\n\n**Not when.** Rows that are themselves the bookings (a stay over several days) \u2014 a `calendar` or `lanes` draws them; a roster\'s row is what the days are filled for.\n\n```json\n{\n "tables": [\n {\n "ref": "nha_si",\n "title": "ho_ten",\n "fields": {\n "ho_ten": {"type": "text"},\n "chuyen_mon": {\n "type": "select",\n "options": [\n {"alias": "tong_quat", "color": "blue"},\n {"alias": "chinh_nha", "color": "purple"},\n {"alias": "nha_chu", "color": "green"},\n {"alias": "phuc_hinh", "color": "orange"}\n ]\n },\n "dien_thoai": {"type": "text"}\n },\n "records": {"subtitle": ["chuyen_mon"], "party": "person"}\n },\n {\n "ref": "lich_truc",\n "title": "ca",\n "fields": {\n "nha_si": {"type": "link", "table": "nha_si", "multi": false},\n "ca": {\n "type": "select",\n "options": [\n {"alias": "sang", "color": "blue"},\n {"alias": "chieu", "color": "purple"},\n {"alias": "ca_ngay", "color": "green"},\n {"alias": "nghi", "color": "gray"}\n ]\n },\n "ngay": {"type": "date"},\n "ghe": {"type": "link", "table": "ghe", "multi": false}\n },\n "records": {"subtitle": ["ngay", "ghe"]}\n }\n ],\n "body": {\n "type": "App",\n "key": "lich_truc",\n "name": "L\u1ECBch tr\u1EF1c",\n "icon": "users",\n "color": "indigo",\n "children": [\n {\n "type": "List",\n "entity": "nha_si",\n "filters": ["chuyen_mon"],\n "layout": "roster",\n "create": ["ho_ten", "chuyen_mon", "dien_thoai"],\n "children": [\n {\n "type": "List",\n "entity": "lich_truc",\n "row": {"subtitle": ["ngay", {"field": "ghe", "add": ["ten", "phong"]}]},\n "create": true,\n "remove": true\n }\n ]\n }\n ]\n }\n}\n```\n\n## Cards\n\n**The job.** Find an item by its picture, and count what the shelf holds.\n\n**Read.** `layout: "cards"` over rows whose `records.image` is a photo; `scope` keeps the app inside one warehouse at a time.\n\n**Keys.** `reference/cards`, `reference/records`, `reference/app-node`\n\n**Not when.** Rows with no picture \u2014 cards of words are a table drawn worse.\n\n```json\n{\n "tables": [\n {\n "ref": "stock_item",\n "title": "name",\n "fields": {\n "name": {"type": "text"},\n "warehouse": {"type": "link", "table": "warehouse", "multi": false},\n "photo": {"type": "files"},\n "sku": {"type": "text"},\n "category": {\n "type": "select",\n "options": [\n {"alias": "pumps", "color": "blue"},\n {"alias": "valves", "color": "teal"},\n {"alias": "seals", "color": "amber"},\n {"alias": "motors", "color": "violet"}\n ]\n },\n "on_hand": {"type": "number"},\n "minimum": {"type": "number"},\n "price": {"type": "number", "notation": {"style": "currency", "currency": "USD"}},\n "supplier_page": {"type": "text", "format": "link"},\n "last_counted": {"type": "date"},\n "counted_by": {"type": "member"},\n "count_note": {"type": "text"}\n },\n "records": {\n "subtitle": ["sku", "category"],\n "image": "photo",\n "figure": "on_hand",\n "limits": {"on_hand": 400},\n "gates": {"on_hand": "minimum"}\n }\n },\n {\n "ref": "order_line",\n "title": "item",\n "fields": {\n "item": {"type": "link", "table": "stock_item", "multi": false},\n "order": {"type": "link", "table": "order", "multi": false},\n "quantity": {"type": "number"},\n "shipped": {"type": "number"},\n "picked": {"type": "number"},\n "unit_price": {"type": "number", "notation": {"style": "currency", "currency": "USD"}},\n "amount": {"type": "formula", "notation": {"style": "currency", "currency": "USD"}},\n "settled": {"type": "formula"},\n "category": {"type": "lookup"}\n },\n "records": {\n "subtitle": ["order", "unit_price"],\n "figure": "amount",\n "limits": {"shipped": "quantity", "picked": "quantity"}\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "stock",\n "name": "Stock",\n "icon": "boxes",\n "color": "teal",\n "scope": "warehouse",\n "children": [\n {\n "type": "List",\n "entity": "stock_item",\n "columns": ["price"],\n "filters": ["on_hand"],\n "search": ["name", "sku"],\n "layout": "cards",\n "children": [\n {"type": "Reading", "key": "register_reading_0", "value": "on_hand", "by": "category"},\n {"type": "Export", "key": "export"},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "item",\n "title": "Item",\n "children": ["price", "minimum", "supplier_page"]\n },\n {\n "type": "Section",\n "key": "last_count",\n "title": "Last count",\n "children": [\n "last_counted",\n "counted_by",\n "count_note",\n {\n "type": "Act",\n "key": "count",\n "label": "Count stock",\n "asks": [\n {"input": "counted", "label": "Units on the shelf", "type": "number", "required": true},\n {"input": "remark", "label": "Remark", "type": "long_text"}\n ],\n "set": {\n "on_hand": {"input": "counted"},\n "last_counted": {"now": true},\n "counted_by": {"me": true},\n "count_note": {"input": "remark"}\n }\n }\n ]\n },\n {\n "type": "Section",\n "key": "on_order",\n "title": "On order",\n "children": [\n {\n "type": "List",\n "entity": "order_line",\n "row": {\n "subtitle": [\n {\n "field": "order",\n "add": [{"field": "customer", "add": ["name", "credit_limit"]}, "reference"]\n },\n "unit_price"\n ]\n },\n "columns": ["quantity"],\n "children": [\n {"type": "Check", "field": "settled", "on": ["update"]},\n {"type": "Record", "children": ["item", "order", "quantity", "shipped", "unit_price"]}\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Party\n\n**The job.** Keep each customer account \u2014 who they are, what they ordered, what was said to them.\n\n**Read.** `records.customer.party: "organization"` and its `image` (the logo); a List with `layout: "timeline"` for the touches, a Reading `by` the day placed for its orders, and its papers `expect`ed by kind.\n\n**Keys.** `reference/records`, `reference/layouts`, `reference/list-node`, `reference/reading-node`\n\n**Not when.** A list of customers is not a job by itself: the customer is a link on the desk whose rows name it, unless the job is the account itself.\n\n```json\n{\n "tables": [\n {\n "ref": "customer",\n "title": "name",\n "fields": {\n "name": {"type": "text"},\n "logo": {"type": "files"},\n "code": {"type": "text"},\n "tier": {\n "type": "select",\n "options": [\n {"alias": "key", "color": "violet"},\n {"alias": "standard", "color": "sky"},\n {"alias": "lapsed", "color": "gray"}\n ]\n },\n "phone": {"type": "text"},\n "website": {"type": "text", "format": "link"},\n "account_manager": {"type": "member"},\n "credit_limit": {"type": "number", "notation": {"style": "currency", "currency": "USD"}},\n "orders": {"type": "link", "table": "order", "multi": true},\n "outstanding": {"type": "rollup"},\n "over_limit": {"type": "formula"},\n "papers_needed": {\n "type": "select",\n "options": [\n {"alias": "credit_application", "color": "blue"},\n {"alias": "trade_reference", "color": "violet"},\n {"alias": "vat_certificate", "color": "teal"}\n ],\n "multi": true\n },\n "notes": {"type": "text", "format": "markdown"}\n },\n "records": {\n "subtitle": ["tier"],\n "image": "logo",\n "party": "organization",\n "figure": "outstanding",\n "limits": {"outstanding": "credit_limit"}\n }\n },\n {\n "ref": "order",\n "title": "reference",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "reference": {"type": "text"},\n "stage": {\n "type": "select",\n "options": [\n {"alias": "draft", "color": "slate"},\n {"alias": "confirmed", "color": "blue"},\n {"alias": "picking", "color": "amber"},\n {"alias": "shipped", "color": "indigo"},\n {"alias": "received", "color": "green"},\n {"alias": "cancelled", "color": "rose"}\n ]\n },\n "placed": {"type": "date"},\n "due_date": {"type": "date"},\n "handler": {"type": "member"},\n "priority": {"type": "boolean"},\n "lines": {"type": "link", "table": "order_line", "multi": true},\n "payments": {"type": "link", "table": "payment", "multi": true},\n "total": {"type": "rollup"},\n "balance": {"type": "formula", "notation": {"style": "currency", "currency": "USD"}},\n "units_ordered": {"type": "rollup"},\n "units_shipped": {"type": "rollup"},\n "units_picked": {"type": "rollup"},\n "carrier": {"type": "text"},\n "tracking_no": {"type": "text"},\n "shipped_on": {"type": "date"},\n "cancellation_reason": {"type": "text"},\n "cancelled_by": {"type": "member"},\n "replaced_by": {"type": "link", "table": "order", "multi": false},\n "attachments": {"type": "files"},\n "delivery_note": {"type": "files"},\n "notes": {"type": "text", "format": "markdown"},\n "paid_in_full": {"type": "formula"},\n "has_payments": {"type": "formula"},\n "reminder_ref": {"type": "text"}\n },\n "records": {\n "subtitle": ["customer", "placed"],\n "figure": "total",\n "limits": {"units_shipped": "units_ordered", "units_picked": "units_ordered"},\n "due": ["due_date"],\n "status": {\n "field": "stage",\n "closed": ["received", "cancelled"],\n "dates": {"shipped": "shipped_on"},\n "history": "order_stage"\n }\n }\n },\n {\n "ref": "customer_paper",\n "title": "kind",\n "fields": {\n "kind": {\n "type": "select",\n "options": [\n {"alias": "credit_application", "color": "blue"},\n {"alias": "trade_reference", "color": "violet"},\n {"alias": "vat_certificate", "color": "teal"}\n ]\n },\n "customer": {"type": "link", "table": "customer", "multi": false},\n "received_on": {"type": "date"},\n "file": {"type": "files"}\n },\n "records": {"image": "file"}\n },\n {\n "ref": "touch",\n "title": "said",\n "fields": {\n "said": {"type": "text"},\n "customer": {"type": "link", "table": "customer", "multi": false},\n "on": {"type": "date"},\n "by": {"type": "member"}\n }\n },\n {\n "ref": "service_visit",\n "title": "customer",\n "fields": {\n "customer": {"type": "link", "table": "customer", "multi": false},\n "scheduled_for": {"type": "date", "format": "datetime"},\n "ends_at": {"type": "date", "format": "datetime"},\n "engineer": {"type": "link", "table": "engineer", "multi": false},\n "job": {\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n "stage": {\n "type": "select",\n "options": [\n {"alias": "booked_on", "color": "blue"},\n {"alias": "parts_ready_on", "color": "blue"},\n {"alias": "arrived_on", "color": "blue"},\n {"alias": "completed_on", "color": "green"}\n ]\n },\n "booked_on": {"type": "date"},\n "parts_ready_on": {"type": "date"},\n "arrived_on": {"type": "date"},\n "hours": {"type": "number", "notation": {"style": "unit", "unit": "h"}},\n "report": {"type": "text", "format": "markdown"},\n "completed_on": {"type": "date"}\n },\n "records": {\n "subtitle": ["scheduled_for", "ends_at"],\n "figure": "hours",\n "due": ["scheduled_for"],\n "status": {\n "field": "stage",\n "closed": ["completed_on"],\n "dates": {\n "booked_on": "booked_on",\n "parts_ready_on": "parts_ready_on",\n "arrived_on": "arrived_on",\n "completed_on": "completed_on"\n }\n }\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "customers",\n "name": "Customers",\n "icon": "users",\n "color": "violet",\n "children": [\n {\n "type": "List",\n "entity": "customer",\n "columns": ["phone", "account_manager"],\n "filters": ["account_manager"],\n "sort": {"field": "name"},\n "opens": {"tier": ["key", "standard"]},\n "create": ["name", "code", "credit_limit"],\n "children": [\n {"type": "Reading", "key": "register_reading_0", "value": "outstanding", "by": "tier"},\n {"type": "Reading", "key": "register_reading_1", "value": "outstanding", "by": "account_manager"},\n {"type": "Check", "field": "over_limit"},\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {"type": "Section", "key": "contact", "title": "Contact", "children": ["phone", "website"]},\n {\n "type": "Section",\n "key": "account",\n "title": "Account",\n "children": [\n "code",\n "account_manager",\n "credit_limit",\n {\n "type": "Act",\n "key": "reassign",\n "label": "Reassign",\n "asks": [{"input": "to", "label": "Account manager", "type": "member", "required": true}],\n "set": {"account_manager": {"input": "to"}}\n },\n {\n "type": "Act",\n "key": "lapse",\n "label": "Mark lapsed",\n "when": {"tier": ["key", "standard"]},\n "set": {"tier": "lapsed"},\n "confirm": true\n },\n {\n "type": "Act",\n "key": "reinstate",\n "label": "Reinstate",\n "when": {"tier": ["lapsed"]},\n "asks": [\n {\n "input": "back_to",\n "label": "Back to",\n "type": "select",\n "options": [\n {"alias": "key", "label": "Key account", "color": "violet"},\n {"alias": "standard", "label": "Standard", "color": "sky"}\n ],\n "required": true\n }\n ],\n "set": {"tier": {"input": "back_to"}}\n }\n ]\n },\n {\n "type": "Section",\n "key": "orders",\n "title": "Orders",\n "children": [\n {\n "type": "Reading",\n "key": "block_reading_0",\n "label": "Order value",\n "entity": "order",\n "value": "total",\n "where": {"stage": ["confirmed", "picking", "shipped", "received"]},\n "by": "placed"\n },\n {\n "type": "List",\n "entity": "order",\n "where": {"stage": ["draft", "confirmed", "picking", "shipped"]},\n "label": "Open orders",\n "columns": ["due_date"],\n "create": false,\n "children": [\n {"type": "Check", "field": "paid_in_full", "on": ["update"]},\n {"type": "Check", "field": "has_payments"},\n {\n "type": "Record",\n "children": [\n "due_date",\n "handler",\n {"field": "priority", "edit": false},\n {\n "field": "lines",\n "add": [\n {\n "field": "item",\n "add": ["name", {"field": "warehouse", "add": ["name", "city"]}, "sku", "category"]\n },\n "quantity"\n ]\n },\n {"field": "payments", "add": true},\n {"field": "carrier", "edit": false},\n {"field": "tracking_no", "edit": false},\n {"field": "shipped_on", "edit": false},\n {"field": "cancellation_reason", "edit": false},\n {"field": "cancelled_by", "edit": false},\n {"field": "replaced_by", "edit": false},\n {"field": "attachments", "edit": false},\n "delivery_note",\n "notes",\n {"field": "reminder_ref", "edit": false}\n ]\n }\n ]\n },\n {\n "type": "List",\n "key": "order_2",\n "entity": "order",\n "where": {"stage": ["shipped", "received"], "paid_in_full": false},\n "label": "Shipped, still owed",\n "columns": ["balance"],\n "create": false,\n "children": [{"type": "Record", "children": ["due_date"]}]\n },\n {\n "type": "List",\n "key": "block_reading_3",\n "entity": "service_visit",\n "app": "visits",\n "label": "Service visits",\n "columns": ["engineer", "job"],\n "children": [\n {\n "type": "Act",\n "key": "clear_visit_hours",\n "label": "Clear hours",\n "set": {"hours": null},\n "confirm": true\n },\n {"type": "Record", "edit": false, "children": ["report", "arrived_on", "completed_on"]}\n ]\n }\n ]\n },\n {\n "type": "Section",\n "key": "account_papers",\n "title": "Account papers",\n "children": [\n {\n "type": "List",\n "entity": "customer_paper",\n "expect": {"field": "kind", "of": "papers_needed"},\n "columns": ["received_on"]\n }\n ]\n },\n {\n "type": "Section",\n "key": "contact_log",\n "title": "Contact log",\n "children": [{"type": "List", "entity": "touch", "layout": "timeline"}]\n },\n {"type": "Section", "key": "notes", "title": "Notes", "children": ["notes"]}\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Case\n\n**The job.** Gather an applicant\'s papers into one case, make the forms it needs, and file it on time.\n\n**Read.** Lists with `expect` for the papers brought (one line per kind, the missing ones empty), an Act\'s `templates` for the forms made (ticked and made at once, kept in `into`), and `intake` reading the papers brought.\n\n**Keys.** `reference/list-node`, `reference/act-node`\n\n**Not when.** A record that needs no papers, or one paper made once \u2014 a single `template` act.\n\n```json\n{\n "tables": [\n {\n "ref": "ho_so",\n "title": "khach_hang",\n "fields": {\n "khach_hang": {"type": "link", "table": "khach_hang", "multi": false},\n "du_an": {"type": "link", "table": "du_an", "multi": false},\n "ma_ho_so": {"type": "autonumber"},\n "dich_vu": {\n "type": "select",\n "options": [{"alias": "tron_goi", "color": "blue"}, {"alias": "lam_ho_so", "color": "violet"}]\n },\n "doi_tuong": {\n "type": "select",\n "options": [\n {"alias": "thu_nhap_thap", "color": "sky"},\n {"alias": "cong_nhan", "color": "teal"},\n {"alias": "can_bo", "color": "indigo"},\n {"alias": "luc_luong", "color": "green"},\n {"alias": "ho_ngheo", "color": "amber"},\n {"alias": "nguoi_co_cong", "color": "rose"}\n ]\n },\n "tinh_trang_nha_o": {\n "type": "select",\n "options": [{"alias": "chua_co_nha", "color": "gray"}, {"alias": "nha_chat", "color": "orange"}]\n },\n "giay_chung_nhan_so": {"type": "text"},\n "dien_tich_nha": {"type": "number"},\n "sale": {"type": "member"},\n "xu_ly": {"type": "member"},\n "can_bo_sung": {"type": "boolean"},\n "phi_dich_vu": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "loai_can": {\n "type": "select",\n "options": [\n {"alias": "studio", "color": "sky"},\n {"alias": "mot_pn", "color": "blue"},\n {"alias": "hai_pn", "color": "indigo"},\n {"alias": "ba_pn", "color": "violet"}\n ]\n },\n "giai_doan": {\n "type": "select",\n "options": [\n {"alias": "ngay_thu_phi", "color": "blue"},\n {"alias": "ngay_nhan_giay_to", "color": "blue"},\n {"alias": "ngay_lap_ho_so", "color": "blue"},\n {"alias": "ngay_nop_cdt", "color": "blue"},\n {"alias": "ngay_cdt_duyet", "color": "blue"},\n {"alias": "ngay_so_xd_duyet", "color": "blue"},\n {"alias": "ngay_boc_tham", "color": "blue"},\n {"alias": "ngay_ky_hdmb", "color": "green"},\n {"alias": "ngay_ban_giao", "color": "green"}\n ]\n },\n "ngay_thu_phi": {"type": "date"},\n "ngay_nhan_giay_to": {"type": "date"},\n "ngay_lap_ho_so": {"type": "date"},\n "ngay_nop_cdt": {"type": "date"},\n "ngay_cdt_duyet": {"type": "date"},\n "ngay_so_xd_duyet": {"type": "date"},\n "ngay_boc_tham": {"type": "date"},\n "ngay_ky_hdmb": {"type": "date"},\n "ngay_ban_giao": {"type": "date"},\n "bo_ho_so": {"type": "files"},\n "giay_to": {"type": "link", "table": "giay_to", "multi": true},\n "thanh_vien": {"type": "link", "table": "thanh_vien", "multi": true},\n "phieu_thu": {"type": "link", "table": "phieu_thu", "multi": true},\n "dien_thoai": {"type": "lookup"},\n "cccd": {"type": "lookup"},\n "ngay_sinh": {"type": "lookup"},\n "dia_chi": {"type": "lookup"},\n "da_thu": {"type": "rollup"},\n "chua_thu_phi": {"type": "formula"},\n "con_no_phi": {"type": "formula"},\n "vuot_thu_nhap": {"type": "formula"},\n "da_hoan_tat": {"type": "formula"}\n },\n "records": {\n "subtitle": ["dich_vu"],\n "figure": "phi_dich_vu",\n "status": {\n "field": "giai_doan",\n "closed": ["ngay_ky_hdmb", "ngay_ban_giao"],\n "dates": {\n "ngay_thu_phi": "ngay_thu_phi",\n "ngay_nhan_giay_to": "ngay_nhan_giay_to",\n "ngay_lap_ho_so": "ngay_lap_ho_so",\n "ngay_nop_cdt": "ngay_nop_cdt",\n "ngay_cdt_duyet": "ngay_cdt_duyet",\n "ngay_so_xd_duyet": "ngay_so_xd_duyet",\n "ngay_boc_tham": "ngay_boc_tham",\n "ngay_ky_hdmb": "ngay_ky_hdmb",\n "ngay_ban_giao": "ngay_ban_giao"\n }\n }\n }\n },\n {\n "ref": "thanh_vien",\n "title": "ho_ten",\n "fields": {\n "ho_ten": {"type": "text"},\n "ho_so": {"type": "link", "table": "ho_so", "multi": false},\n "quan_he": {\n "type": "select",\n "options": [\n {"alias": "nguoi_dung_don", "color": "blue"},\n {"alias": "vo_chong", "color": "violet"},\n {"alias": "con", "color": "teal"},\n {"alias": "bo_me", "color": "amber"},\n {"alias": "khac", "color": "gray"}\n ]\n },\n "ngay_sinh": {"type": "date"},\n "cccd": {"type": "text"},\n "thu_nhap": {"type": "number", "notation": {"style": "currency", "currency": "VND"}}\n },\n "records": {"subtitle": ["quan_he", "cccd"], "party": "person", "figure": "thu_nhap"}\n },\n {\n "ref": "giay_to",\n "title": "loai",\n "fields": {\n "loai": {\n "type": "select",\n "options": [\n {"alias": "don_dang_ky", "color": "blue"},\n {"alias": "can_cuoc", "color": "sky"},\n {"alias": "giay_doi_tuong", "color": "violet"},\n {"alias": "xac_nhan_nha_o", "color": "teal"},\n {"alias": "xac_nhan_thu_nhap", "color": "amber"},\n {"alias": "hon_nhan", "color": "indigo"}\n ]\n },\n "ho_so": {"type": "link", "table": "ho_so", "multi": false},\n "ngay_nhan": {"type": "date"},\n "tep": {"type": "files"},\n "phai_sua": {"type": "formula"}\n },\n "records": {"image": "tep", "starts": {"ngay_nhan": "today"}}\n },\n {\n "ref": "phieu_thu",\n "title": "khoan",\n "fields": {\n "khoan": {\n "type": "select",\n "options": [\n {"alias": "dat_coc", "color": "blue"},\n {"alias": "tat_toan", "color": "indigo"},\n {"alias": "phi_ho_so", "color": "violet"}\n ]\n },\n "ho_so": {"type": "link", "table": "ho_so", "multi": false},\n "so_tien": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "hinh_thuc": {\n "type": "select",\n "options": [\n {"alias": "tien_mat", "color": "green", "mark": {"kind": "icon", "name": "banknote"}},\n {"alias": "chuyen_khoan", "color": "blue", "mark": {"kind": "icon", "name": "arrow-right-left"}}\n ]\n },\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "cho_thu", "color": "amber"},\n {"alias": "da_thu", "color": "green"},\n {"alias": "huy", "color": "gray"}\n ]\n },\n "ngay_thu": {"type": "date"}\n },\n "records": {\n "subtitle": ["hinh_thuc", "ngay_thu"],\n "figure": "so_tien",\n "status": {"field": "trang_thai", "closed": ["da_thu", "huy"]}\n }\n }\n ],\n "templates": [\n {"alias": "don_dang_ky", "type": "html", "label": "\u0110\u01A1n \u0111\u0103ng k\xFD mua nh\xE0 \u1EDF x\xE3 h\u1ED9i"},\n {"alias": "mau_02_chua_co_nha", "type": "html", "label": "M\u1EABu 02 \u2014 X\xE1c nh\u1EADn ch\u01B0a c\xF3 nh\xE0 \u1EDF"},\n {"alias": "mau_03_nha_chat", "type": "html", "label": "M\u1EABu 03 \u2014 X\xE1c nh\u1EADn nh\xE0 \u1EDF d\u01B0\u1EDBi 15 m\xB2/ng\u01B0\u1EDDi"},\n {"alias": "mau_05_thu_nhap", "type": "html", "label": "M\u1EABu 05 \u2014 X\xE1c nh\u1EADn thu nh\u1EADp"},\n {"alias": "danh_sach_nop", "type": "html", "label": "Danh s\xE1ch h\u1ED3 s\u01A1 n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0"}\n ],\n "body": {\n "type": "App",\n "key": "ho_so",\n "name": "H\u1ED3 s\u01A1 nh\xE0 \u1EDF x\xE3 h\u1ED9i",\n "icon": "folder-open",\n "color": "blue",\n "scope": "du_an",\n "children": [\n {\n "type": "List",\n "entity": "ho_so",\n "row": {"title": {"add": true}},\n "columns": ["xu_ly", "can_bo_sung", "doi_tuong"],\n "filters": ["xu_ly", "can_bo_sung"],\n "search": ["khach_hang", "ma_ho_so", "dien_thoai"],\n "create": ["khach_hang", "dich_vu", "doi_tuong", "tinh_trang_nha_o", "loai_can", "phi_dich_vu", "sale"],\n "children": [\n {\n "type": "Reading",\n "key": "register_reading_0",\n "label": "Ch\u1EDD kh\xE1ch b\u1ED5 sung",\n "where": {"can_bo_sung": true}\n },\n {\n "type": "Reading",\n "key": "register_reading_1",\n "label": "Ph\xED \u0111\xE3 thu",\n "value": "da_thu",\n "over": "ngay_nhan_giay_to"\n },\n {"type": "Reading", "key": "register_reading_2", "by": "doi_tuong"},\n {"type": "Export", "key": "export"},\n {"type": "Check", "field": "chua_thu_phi", "on": ["nop_cdt"]},\n {"type": "Check", "field": "con_no_phi"},\n {"type": "Check", "field": "vuot_thu_nhap"},\n {"type": "Check", "field": "da_hoan_tat", "on": ["doc_giay_to"]},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "nguoi_dung_don",\n "title": "Ng\u01B0\u1EDDi \u0111\u1EE9ng \u0111\u01A1n",\n "children": [\n "ma_ho_so",\n "dien_thoai",\n "cccd",\n "ngay_sinh",\n "dia_chi",\n "doi_tuong",\n "tinh_trang_nha_o",\n "sale",\n "xu_ly",\n "can_bo_sung",\n "ngay_ky_hdmb",\n "ngay_ban_giao",\n {\n "type": "List",\n "entity": "thanh_vien",\n "label": "H\u1ED9 gia \u0111\xECnh",\n "columns": ["ngay_sinh"],\n "create": ["ho_ten", "quan_he", "ngay_sinh", "cccd", "thu_nhap"]\n },\n {\n "type": "List",\n "entity": "giay_to",\n "expect": {"field": "loai", "only": ["can_cuoc", "hon_nhan"]},\n "label": "Gi\u1EA5y t\u1EDD c\xE1 nh\xE2n",\n "columns": ["ngay_nhan"],\n "children": [{"type": "Check", "field": "phai_sua"}]\n },\n {\n "type": "Act",\n "key": "doc_giay_to",\n "label": "\u0110\u1ECDc gi\u1EA5y t\u1EDD",\n "intake": "giay_to",\n "fills": [\n "doi_tuong",\n "tinh_trang_nha_o",\n "giay_chung_nhan_so",\n "dien_tich_nha",\n "khach_hang.ho_ten",\n "khach_hang.cccd",\n "khach_hang.ngay_sinh",\n "khach_hang.dia_chi",\n "thanh_vien"\n ]\n },\n {\n "type": "Act",\n "key": "giao_xu_ly",\n "label": "Giao ng\u01B0\u1EDDi x\u1EED l\xFD",\n "asks": [{"input": "nguoi", "label": "Ng\u01B0\u1EDDi x\u1EED l\xFD", "type": "member", "required": true}],\n "set": {"xu_ly": {"input": "nguoi"}}\n },\n {\n "type": "Act",\n "key": "chuyen_du_an",\n "label": "Chuy\u1EC3n d\u1EF1 \xE1n",\n "asks": [\n {\n "input": "du_an_moi",\n "label": "Sang d\u1EF1 \xE1n",\n "type": "link",\n "entity": "du_an",\n "required": true,\n "add": ["ten_du_an", "chu_dau_tu", "so_can"]\n }\n ],\n "set": {"du_an": {"input": "du_an_moi"}},\n "confirm": true\n }\n ]\n },\n {\n "type": "Section",\n "key": "bo_ho_so",\n "title": "B\u1ED9 h\u1ED3 s\u01A1",\n "at": ["ngay_nhan_giay_to", "ngay_lap_ho_so"],\n "children": [\n "ngay_nhan_giay_to",\n "ngay_lap_ho_so",\n {\n "type": "Act",\n "key": "tao_giay_to",\n "label": "T\u1EA1o gi\u1EA5y t\u1EDD",\n "templates": [\n {"template": "don_dang_ky"},\n {"template": "mau_02_chua_co_nha", "when": {"tinh_trang_nha_o": ["chua_co_nha"]}},\n {"template": "mau_03_nha_chat", "when": {"tinh_trang_nha_o": ["nha_chat"]}},\n {"template": "mau_05_thu_nhap", "when": {"doi_tuong": ["thu_nhap_thap"]}}\n ],\n "into": "bo_ho_so",\n "when": {"giai_doan": ["ngay_nhan_giay_to", "ngay_lap_ho_so"]}\n },\n {\n "type": "Act",\n "key": "nop_cdt",\n "label": "N\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "when": {"dich_vu": ["tron_goi"], "giai_doan": ["ngay_lap_ho_so"]},\n "requires": ["bo_ho_so", "loai_can"],\n "asks": ["loai_can"],\n "set": {"giai_doan": "ngay_nop_cdt"},\n "confirm": true\n },\n {\n "type": "Act",\n "key": "to_ngay_nhan_giay_to",\n "label": "\u0110\xE3 nh\u1EADn gi\u1EA5y t\u1EDD",\n "when": {"giai_doan": {"empty": true}, "dich_vu": ["tron_goi"]},\n "asks": [{"field": "ngay_nhan_giay_to", "starts": "today"}],\n "set": {"giai_doan": "ngay_nhan_giay_to"}\n },\n {\n "type": "Act",\n "key": "to_ngay_lap_ho_so",\n "label": "\u0110\xE3 l\u1EADp h\u1ED3 s\u01A1",\n "when": {"giai_doan": ["ngay_nhan_giay_to"], "dich_vu": ["tron_goi"]},\n "asks": [{"field": "ngay_lap_ho_so", "starts": "today"}],\n "set": {"giai_doan": "ngay_lap_ho_so"}\n },\n {\n "type": "Act",\n "key": "to_ngay_ban_giao",\n "label": "\u0110\xE3 b\xE0n giao h\u1ED3 s\u01A1",\n "when": {"giai_doan": ["ngay_nhan_giay_to"], "dich_vu": ["lam_ho_so"]},\n "asks": [{"field": "ngay_ban_giao", "starts": "today"}],\n "set": {"giai_doan": "ngay_ban_giao"}\n }\n ]\n }\n ]\n },\n {\n "type": "Act",\n "key": "in_danh_sach",\n "label": "In danh s\xE1ch n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "on": "view",\n "template": "danh_sach_nop"\n }\n ]\n }\n ]\n }\n}\n```\n\n## Calls\n\n**The job.** Call a lead from their record, and keep what was said as a touch on its timeline.\n\n**Read.** An Act\'s `record` (into the `timeline` child: audio, transcript, when and who) and `fills` (the touch\'s fields an agent fills from the transcript).\n\n**Keys.** `reference/act-node`, `reference/layouts`\n\n**Not when.** Calls made and logged elsewhere \u2014 a `timeline` with its own add keeps the note.\n\n```json\n{\n "tables": [\n {\n "ref": "khach_tiem_nang",\n "title": "ho_ten",\n "fields": {\n "ho_ten": {"type": "text"},\n "ten_cong_ty": {"type": "text"},\n "vai_tro": {\n "type": "select",\n "options": [\n {"alias": "chu_doanh_nghiep", "color": "emerald"},\n {"alias": "ke_toan_truong", "color": "blue"},\n {"alias": "quan_ly", "color": "teal"},\n {"alias": "nhan_vien", "color": "sky"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n "loai_hinh": {\n "type": "select",\n "options": [\n {"alias": "thuong_mai", "color": "blue"},\n {"alias": "san_xuat", "color": "amber"},\n {"alias": "dich_vu", "color": "violet"},\n {"alias": "xay_dung", "color": "orange"},\n {"alias": "ho_kinh_doanh", "color": "teal"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n "quy_mo": {\n "type": "select",\n "options": [\n {"alias": "q_1_10", "color": "sky"},\n {"alias": "q_11_50", "color": "blue"},\n {"alias": "q_51_200", "color": "indigo"},\n {"alias": "q_200", "color": "violet"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n "so_dien_thoai": {"type": "text"},\n "zalo": {"type": "text"},\n "email": {"type": "text"},\n "trang_ca_nhan": {"type": "text", "format": "link"},\n "anh": {"type": "files"},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "moi", "color": "gray"},\n {"alias": "du_dieu_kien", "color": "blue"},\n {"alias": "da_lien_he", "color": "amber"},\n {"alias": "da_chuyen_doi", "color": "emerald"},\n {"alias": "loai", "color": "red"}\n ]\n },\n "ly_do_loai": {\n "type": "select",\n "options": [\n {"alias": "khong_phu_hop", "color": "orange"},\n {"alias": "qua_nho", "color": "amber"},\n {"alias": "chua_co_ngan_sach", "color": "yellow"},\n {"alias": "sai_nguoi", "color": "blue"},\n {"alias": "yeu_cau_dung", "color": "red"},\n {"alias": "trung", "color": "zinc"}\n ]\n },\n "vi_sao_du_dieu_kien": {"type": "text"},\n "phu_trach": {"type": "member"},\n "nguon": {"type": "link", "table": "kenh", "multi": false},\n "lien_he": {"type": "link", "table": "lien_he", "multi": false},\n "cong_ty": {"type": "link", "table": "cong_ty", "multi": false},\n "ngay_chuyen_doi": {"type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n "ghi_chu": {"type": "text", "format": "markdown"},\n "tin_hieu": {"type": "link", "table": "tin_hieu", "multi": true},\n "buoc_tiep": {"type": "link", "table": "buoc_tiep", "multi": true},\n "tuong_tac": {"type": "link", "table": "tuong_tac", "multi": true},\n "do_nong": {\n "type": "formula",\n "options": [\n {"alias": "nong", "label": "N\xF3ng", "color": "orange"},\n {"alias": "nguoi", "label": "Ngu\u1ED9i", "color": "sky"}\n ]\n },\n "han_buoc_tiep": {"type": "rollup"},\n "khong_lien_he": {"type": "formula"}\n },\n "records": {\n "subtitle": ["ten_cong_ty"],\n "image": "anh",\n "party": "person",\n "due": ["han_buoc_tiep"],\n "status": {\n "field": "trang_thai",\n "closed": ["da_chuyen_doi", "loai"],\n "dates": {"da_chuyen_doi": "ngay_chuyen_doi"}\n }\n }\n },\n {\n "ref": "tin_hieu",\n "title": "tom_tat",\n "fields": {\n "tom_tat": {"type": "text"},\n "noi_dung": {"type": "text", "format": "markdown"},\n "anh_chup": {"type": "files"},\n "lien_ket": {"type": "text", "format": "link"},\n "kenh": {"type": "link", "table": "kenh", "multi": false},\n "loai": {\n "type": "select",\n "options": [\n {"alias": "hoi_dich_vu", "color": "emerald"},\n {"alias": "than_phien", "color": "blue"},\n {"alias": "tuyen_ke_toan", "color": "amber"},\n {"alias": "moi_thanh_lap", "color": "violet"},\n {"alias": "gioi_thieu", "color": "teal"},\n {"alias": "danh_ba", "color": "zinc"},\n {"alias": "nhieu", "color": "stone"}\n ]\n },\n "ngay_dang": {"type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n "ngay_xay_ra": {"type": "formula"}\n },\n "records": {"subtitle": ["kenh", "ngay_xay_ra"], "image": "anh_chup"}\n },\n {\n "ref": "buoc_tiep",\n "title": "vi_sao_luc_nay",\n "fields": {\n "vi_sao_luc_nay": {"type": "text"},\n "viec": {\n "type": "select",\n "options": [\n {"alias": "binh_luan", "color": "blue", "mark": {"kind": "icon", "name": "message-square"}},\n {"alias": "nhan_facebook", "color": "violet", "mark": {"kind": "brand", "name": "facebook"}},\n {"alias": "nhan_zalo", "color": "sky", "mark": {"kind": "brand", "name": "zalo"}},\n {"alias": "goi_dien", "color": "teal", "mark": {"kind": "icon", "name": "phone"}},\n {"alias": "gui_email", "color": "indigo", "mark": {"kind": "icon", "name": "mail"}},\n {"alias": "gui_bang_gia", "color": "amber", "mark": {"kind": "icon", "name": "receipt"}}\n ]\n },\n "ban_nhap": {"type": "text", "format": "markdown"},\n "han": {"type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n "tinh_trang": {\n "type": "select",\n "options": [\n {"alias": "cho", "color": "blue"},\n {"alias": "xong", "color": "emerald"},\n {"alias": "bo_qua", "color": "zinc"}\n ]\n }\n },\n "records": {\n "subtitle": ["viec"],\n "mark": "viec",\n "due": ["han"],\n "status": {"field": "tinh_trang", "closed": ["xong", "bo_qua"]}\n }\n },\n {\n "ref": "tuong_tac",\n "title": "tom_tat",\n "fields": {\n "tom_tat": {"type": "text"},\n "thoi_diem": {"type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n "qua": {\n "type": "select",\n "options": [\n {"alias": "goi_dien", "color": "teal", "mark": {"kind": "icon", "name": "phone"}},\n {"alias": "zalo", "color": "sky", "mark": {"kind": "brand", "name": "zalo"}},\n {"alias": "facebook", "color": "blue", "mark": {"kind": "brand", "name": "facebook"}},\n {"alias": "email", "color": "indigo", "mark": {"kind": "icon", "name": "mail"}},\n {"alias": "gap_truc_tiep", "color": "emerald", "mark": {"kind": "icon", "name": "users"}},\n {"alias": "khac", "color": "zinc", "mark": {"kind": "icon", "name": "ellipsis"}}\n ]\n },\n "phan_hoi": {\n "type": "select",\n "options": [\n {"alias": "dang_cho", "color": "blue"},\n {"alias": "da_tra_loi", "color": "green"},\n {"alias": "quan_tam", "color": "amber"},\n {"alias": "khong_phu_hop", "color": "red"},\n {"alias": "khong_phan_hoi", "color": "zinc"},\n {"alias": "sai_so", "color": "stone"}\n ]\n },\n "ghi_am": {"type": "files"},\n "nguyen_van": {"type": "text", "format": "markdown"},\n "hen_lai": {"type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n "nguoi_thuc_hien": {"type": "member"}\n },\n "records": {"subtitle": ["qua", "phan_hoi"]}\n }\n ],\n "body": {\n "type": "App",\n "key": "khach_tiem_nang",\n "name": "Kh\xE1ch ti\u1EC1m n\u0103ng",\n "icon": "radar",\n "color": "violet",\n "children": [\n {\n "type": "List",\n "entity": "khach_tiem_nang",\n "columns": ["phu_trach", "han_buoc_tiep"],\n "filters": ["phu_trach"],\n "create": [\n "ho_ten",\n "trang_ca_nhan",\n "so_dien_thoai",\n "ten_cong_ty",\n {"field": "nguon", "add": ["ten", "loai"]}\n ],\n "children": [\n {"type": "Reading", "key": "register_reading_0", "by": "phu_trach"},\n {\n "type": "Reading",\n "key": "register_reading_1",\n "label": "Kh\xE1ch chuy\u1EC3n \u0111\u1ED5i",\n "by": "ngay_chuyen_doi"\n },\n {"type": "Check", "field": "khong_lien_he", "resolve": "da_lien_he"},\n {\n "type": "Record",\n "children": [\n {\n "type": "Section",\n "key": "danh_gia",\n "title": "\u0110\xE1nh gi\xE1",\n "at": ["moi"],\n "children": [\n "vai_tro",\n "loai_hinh",\n "quy_mo",\n "vi_sao_du_dieu_kien",\n "do_nong",\n {\n "type": "List",\n "entity": "tin_hieu",\n "label": "T\xEDn hi\u1EC7u",\n "columns": ["loai", "noi_dung"],\n "create": ["tom_tat", "anh_chup", "loai", "noi_dung", "kenh", "lien_ket", "ngay_dang"]\n },\n {\n "type": "Act",\n "key": "danh_gia_dat",\n "label": "\u0110\u1EE7 \u0111i\u1EC1u ki\u1EC7n",\n "when": {"trang_thai": ["moi"]},\n "requires": ["vi_sao_du_dieu_kien"],\n "set": {"trang_thai": "du_dieu_kien"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "lien_he",\n "title": "Li\xEAn h\u1EC7",\n "at": ["du_dieu_kien"],\n "children": [\n "so_dien_thoai",\n "phu_trach",\n "trang_ca_nhan",\n "zalo",\n "email",\n {"field": "ghi_chu", "block": false},\n {\n "type": "List",\n "entity": "buoc_tiep",\n "label": "B\u01B0\u1EDBc ti\u1EBFp theo",\n "columns": ["ban_nhap"],\n "create": ["vi_sao_luc_nay", "viec", "han", "ban_nhap"],\n "children": [\n {\n "type": "Act",\n "key": "lam_xong",\n "label": "\u0110\xE3 l\xE0m",\n "when": {"tinh_trang": ["cho"]},\n "set": {"tinh_trang": "xong"}\n }\n ]\n },\n {\n "type": "List",\n "entity": "tuong_tac",\n "label": "T\u01B0\u01A1ng t\xE1c",\n "layout": "timeline",\n "create": ["tom_tat", "qua", "phan_hoi", "thoi_diem", "hen_lai"]\n },\n {\n "type": "Act",\n "key": "ghi_cuoc_goi",\n "label": "Ghi \xE2m cu\u1ED9c g\u1ECDi",\n "when": {"trang_thai": ["du_dieu_kien"]},\n "fills": ["tom_tat", "phan_hoi"],\n "record": {\n "into": "tuong_tac",\n "audio": "ghi_am",\n "transcript": "nguyen_van",\n "at": "thoi_diem",\n "by": "nguoi_thuc_hien"\n }\n },\n {\n "type": "Act",\n "key": "da_lien_he",\n "label": "\u0110\xE1nh d\u1EA5u \u0111\xE3 li\xEAn h\u1EC7",\n "when": {"trang_thai": ["du_dieu_kien"]},\n "set": {"trang_thai": "da_lien_he"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "chuyen_doi",\n "title": "Chuy\u1EC3n \u0111\u1ED5i",\n "at": ["da_lien_he"],\n "children": [\n "nguon",\n {"field": "lien_he", "edit": false},\n {"field": "cong_ty", "edit": false},\n {\n "type": "Act",\n "key": "chuyen_doi",\n "label": "Chuy\u1EC3n \u0111\u1ED5i",\n "when": {"trang_thai": ["da_lien_he"]},\n "workflow": "chuyen_doi_khach",\n "confirm": true\n }\n ]\n },\n {\n "type": "Section",\n "key": "loai",\n "title": "Lo\u1EA1i",\n "at": ["loai"],\n "children": [\n "ly_do_loai",\n {\n "type": "Act",\n "key": "loai",\n "label": "Lo\u1EA1i",\n "when": {"trang_thai": ["moi", "du_dieu_kien", "da_lien_he"]},\n "asks": ["ly_do_loai"],\n "set": {"trang_thai": "loai"},\n "danger": true\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Approval\n\n**The job.** Raise a payment request, see it through approval, and read what is waiting at the top of the list.\n\n**Read.** `records.de_nghi.status` with its `history` (each move: when and who), Sections staged by `at`, and the List\'s Readings leading with the headline number. The history stays because the business reads who signed each move; where only the day a stage was reached matters, the status stamps a date on the row instead (the Gantt above).\n\n**Keys.** `reference/status`, `reference/section-node`, `reference/reading-node`\n\n**Not when.** Rows that never move \u2014 a reference list (a price list, a catalog) has no status and no headline number.\n\n```json\n{\n "tables": [\n {\n "ref": "de_nghi",\n "title": "noi_dung",\n "fields": {\n "noi_dung": {"type": "text"},\n "so_de_nghi": {"type": "text"},\n "loai": {\n "type": "select",\n "options": [\n {"alias": "thanh_toan_ncc", "color": "violet"},\n {"alias": "nhan_cong", "color": "indigo"},\n {"alias": "chi_phi_cong_truong", "color": "sky"},\n {"alias": "tam_ung", "color": "amber"},\n {"alias": "hoan_ung", "color": "teal"}\n ]\n },\n "du_an": {"type": "link", "table": "du_an", "multi": false},\n "nha_cung_cap": {"type": "link", "table": "nha_cung_cap", "multi": false},\n "so_tien": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "han_thanh_toan": {"type": "date"},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "nhap", "color": "slate"},\n {"alias": "cho_duyet", "color": "amber"},\n {"alias": "cho_chu_tich", "color": "violet"},\n {"alias": "da_duyet", "color": "blue"},\n {"alias": "da_chi", "color": "green"},\n {"alias": "tu_choi", "color": "rose"}\n ]\n },\n "nguoi_de_nghi": {"type": "member"},\n "chung_tu": {"type": "files"},\n "nguoi_duyet": {"type": "member"},\n "duyet_luc": {"type": "date", "format": "datetime"},\n "ly_do_tu_choi": {"type": "text"},\n "phieu_chi": {"type": "files"},\n "ngan_sach_du_an": {"type": "lookup"},\n "da_duyet_du_an": {"type": "lookup"},\n "vuot_ngan_sach": {"type": "formula"},\n "sat_ngan_sach": {"type": "formula"},\n "da_gui": {"type": "formula"},\n "nhap_at": {"type": "date", "format": "datetime"},\n "cho_duyet_at": {"type": "date", "format": "datetime"},\n "cho_chu_tich_at": {"type": "date", "format": "datetime"},\n "da_duyet_at": {"type": "date", "format": "datetime"}\n },\n "records": {\n "subtitle": ["du_an"],\n "figure": "so_tien",\n "limits": {"da_duyet_du_an": "ngan_sach_du_an"},\n "due": ["han_thanh_toan"],\n "starts": {"nguoi_de_nghi": "me"},\n "status": {\n "field": "trang_thai",\n "closed": ["da_chi", "tu_choi"],\n "dropped": ["tu_choi"],\n "dates": {\n "nhap": "nhap_at",\n "cho_duyet": "cho_duyet_at",\n "cho_chu_tich": "cho_chu_tich_at",\n "da_duyet": "da_duyet_at"\n },\n "history": "lich_su_duyet",\n "stages": {\n "nhap": {"owner": "nguoi_de_nghi"},\n "cho_duyet": {"owner": {"group": "<id of the giam_doc group>"}, "within": 1},\n "cho_chu_tich": {\n "owner": {"group": "<id of the chu_tich group>"},\n "within": 2,\n "when": {"so_tien": {"gt": 200000000}}\n },\n "da_duyet": {"owner": {"group": "<id of the ke_toan group>"}, "within": 2}\n }\n }\n }\n }\n ],\n "body": {\n "type": "App",\n "key": "de_nghi_chi",\n "name": "\u0110\u1EC1 ngh\u1ECB thanh to\xE1n",\n "icon": "hand-coins",\n "color": "amber",\n "children": [\n {\n "type": "List",\n "entity": "de_nghi",\n "row": {"subtitle": [{"field": "du_an", "add": ["ten", "ma_du_an", "chu_dau_tu", "ngan_sach"]}]},\n "columns": ["nguoi_de_nghi"],\n "filters": ["nguoi_de_nghi", "du_an"],\n "group": "du_an",\n "create": [\n "noi_dung",\n "loai",\n "du_an",\n {"field": "nha_cung_cap", "add": true},\n "so_tien",\n "han_thanh_toan",\n "nguoi_de_nghi"\n ],\n "children": [\n {\n "type": "Reading",\n "key": "register_reading_0",\n "label": "\u0110\xE3 duy\u1EC7t, ch\u1EDD chi",\n "value": "so_tien",\n "where": {"trang_thai": ["da_duyet"]}\n },\n {"type": "Reading", "key": "register_reading_1", "value": "so_tien", "by": "han_thanh_toan"},\n {"type": "Check", "field": "da_gui", "on": ["update"]},\n {"type": "Check", "field": "vuot_ngan_sach"},\n {"type": "Check", "field": "sat_ngan_sach"},\n {\n "type": "Record",\n "door": "drawer",\n "children": [\n {\n "type": "Section",\n "key": "de_nghi",\n "title": "\u0110\u1EC1 ngh\u1ECB",\n "at": ["nhap"],\n "children": [\n "so_de_nghi",\n "loai",\n "nha_cung_cap",\n "han_thanh_toan",\n "nguoi_de_nghi",\n {"field": "chung_tu", "label": "Ch\u1EE9ng t\u1EEB k\xE8m"},\n {\n "type": "Act",\n "key": "gui_duyet",\n "label": "G\u1EEDi duy\u1EC7t",\n "when": {"trang_thai": ["nhap"]},\n "requires": ["chung_tu", "nguoi_de_nghi"],\n "set": {"trang_thai": "cho_duyet"}\n }\n ]\n },\n {\n "type": "Section",\n "key": "duyet",\n "title": "Duy\u1EC7t",\n "at": ["cho_duyet", "cho_chu_tich"],\n "children": [\n {"field": "nguoi_duyet", "edit": false},\n {"field": "duyet_luc", "edit": false},\n {"field": "ly_do_tu_choi", "edit": false},\n {\n "type": "Act",\n "key": "rut_lai",\n "label": "R\xFAt l\u1EA1i",\n "when": {"trang_thai": ["cho_duyet", "cho_chu_tich"]},\n "set": {"trang_thai": "nhap"},\n "confirm": true\n }\n ]\n },\n {\n "type": "Section",\n "key": "da_chi",\n "title": "\u0110\xE3 chi",\n "at": ["da_chi"],\n "children": [{"field": "phieu_chi", "label": "Phi\u1EBFu chi", "edit": false}]\n }\n ]\n }\n ]\n }\n ]\n }\n}\n```\n\n## Dashboard\n\n**The job.** The owner\'s question \u2014 what came in, what went out, where it went, and what is still unmatched \u2014 answered over one period.\n\n**Read.** An App of Readings and no List: Readings with `over` (the period windows them), a Reading `by` the day, Readings `by` a field, and a List of rows to act on that opens in its `app`.\n\n**Keys.** `reference/app-node`, `reference/reading-node`\n\n**Not when.** One job\'s headline numbers \u2014 those lead that job\'s List as its Readings.\n\n```json\n{\n "tables": [\n {\n "ref": "giao_dich",\n "title": "noi_dung",\n "fields": {\n "noi_dung": {"type": "text"},\n "ngay": {"type": "date"},\n "loai": {"type": "select", "options": [{"alias": "thu", "color": "green"}, {"alias": "chi", "color": "rose"}]},\n "so_tien": {"type": "number", "notation": {"style": "currency", "currency": "VND"}},\n "bien_dong": {"type": "formula", "notation": {"style": "currency", "currency": "VND"}},\n "doi_tuong": {"type": "link", "table": "doi_tuong", "multi": false},\n "danh_muc": {\n "type": "select",\n "options": [\n {"alias": "doanh_thu", "color": "green"},\n {"alias": "hang_hoa", "color": "blue"},\n {"alias": "van_chuyen", "color": "indigo"},\n {"alias": "nhan_su", "color": "violet"},\n {"alias": "van_phong", "color": "sky"},\n {"alias": "thue_phi", "color": "slate"},\n {"alias": "ngan_hang", "color": "teal"},\n {"alias": "khac", "color": "gray"}\n ]\n },\n "sao_ke": {"type": "link", "table": "sao_ke", "multi": false},\n "hoa_don": {"type": "link", "table": "hoa_don", "multi": true},\n "trang_thai": {\n "type": "select",\n "options": [\n {"alias": "chua_khop", "color": "amber"},\n {"alias": "da_khop", "color": "green"},\n {"alias": "khong_can_hd", "color": "slate"}\n ]\n },\n "khop_luc": {"type": "date", "format": "datetime"}\n },\n "records": {\n "subtitle": ["ngay"],\n "figure": "bien_dong",\n "status": {"field": "trang_thai", "closed": ["da_khop", "khong_can_hd"], "dates": {"da_khop": "khop_luc"}}\n }\n },\n {\n "ref": "hoa_don",\n "title": "doi_tuong",\n "fields": {\n "doi_tuong": {"type": "link", "table": "doi_tuong", "multi": false},\n "so_hoa_don": {"type": "text"},\n "ngay_hoa_don": {"type": "date"},\n "so_ngay_qua_han": {"type": "formula", "notation": {"style": "unit", "unit": "day"}},\n "chieu": {\n "type": "select",\n "options": [{"alias": "ban_ra", "color": "green"}, {"alias": "mua_vao", "color": "violet"}]\n },\n "tong_cong": {"type": "formula", "notation": {"style": "currency", "currency": "VND"}},\n "thue_suat": {"type": "formula", "notation": {"style": "unit", "unit": "percent"}},\n "tep": {"type": "files"},\n "giao_dich": {"type": "link", "table": "giao_dich", "multi": true}\n },\n "records": {"subtitle": ["so_hoa_don", "ngay_hoa_don"], "image": "tep", "figure": "tong_cong"}\n }\n ],\n "body": {\n "type": "App",\n "key": "dong_tien",\n "name": "D\xF2ng ti\u1EC1n",\n "icon": "chart-line",\n "color": "emerald",\n "children": [\n {\n "type": "Reading",\n "key": "dashboard_reading_0",\n "label": "Ti\u1EC1n v\xE0o",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "over": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_1",\n "label": "Ti\u1EC1n ra",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "over": "ngay",\n "better": "down"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_2",\n "label": "D\xF2ng ti\u1EC1n r\xF2ng",\n "entity": "giao_dich",\n "value": "bien_dong",\n "by": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_3",\n "label": "Chi theo danh m\u1EE5c",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "by": "danh_muc",\n "over": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_4",\n "label": "Thu theo kh\xE1ch h\xE0ng",\n "entity": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "by": "doi_tuong",\n "over": "ngay"\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_5",\n "label": "C\xF4ng n\u1EE3 b\xE1n ra theo tu\u1ED5i n\u1EE3",\n "entity": "hoa_don",\n "value": "tong_cong",\n "where": {"chieu": ["ban_ra"]},\n "by": "doi_tuong",\n "across": {"field": "so_ngay_qua_han", "tiers": [1, 8, 16, 31]}\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_6",\n "label": "S\u1ED1 ng\xE0y qu\xE1 h\u1EA1n b\xECnh qu\xE2n",\n "entity": "hoa_don",\n "value": {"avg": "so_ngay_qua_han"},\n "where": {"chieu": ["ban_ra"]}\n },\n {\n "type": "Reading",\n "key": "dashboard_reading_7",\n "label": "Thu\u1EBF su\u1EA5t b\xECnh qu\xE2n",\n "entity": "hoa_don",\n "value": {"avg": "thue_suat"}\n },\n {\n "type": "List",\n "key": "dashboard_reading_8",\n "entity": "giao_dich",\n "where": {"trang_thai": ["chua_khop"]},\n "app": "doi_chieu",\n "label": "D\xF2ng ch\u01B0a kh\u1EDBp",\n "columns": ["sao_ke"]\n }\n ]\n }\n}\n```\n';
1769
1769
 
1770
1770
  // docs/agent_surface.md
1771
- var agent_surface_default = '# An app\'s agent surface\n\nEverything a person reads or does in an app, an agent reads or does through that app\'s **queries** and\n**workflows** \u2014 the same ones its screen runs, under the same permissions and the same rules. Three tools\nreach them, over MCP and as `lotics run <tool>`:\n\n| Tool | Answers |\n|---|---|\n| `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`) \u2014 beside what each table they name holds, in its owner\'s words |\n| `run_app_query` | The rows, a count or the groups of one query \u2014 or of several in one call |\n| `run_app_workflow` | One write \u2014 or several in order \u2014 or a dry run of it that writes nothing |\n\nAn app generates this surface from its tree; Query and Workflow nodes declare what the screen never\nasks (\xA7 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\nLists and its Acts are queries and workflows of the same app, named by the log\'s table as the register\'s\nare by its own \u2014 `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\n(`app_bindings`).\n\n## Finding what an app offers\n\n`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`.\n\n## What every app generates\n\nA 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\napp has.\n\n### Queries\n\n<!-- generated:start queries -->\n\n| Called | What it says of itself | Takes | Generated for |\n|---|---|---|---|\n| `<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 \u2014 a select, a one-row link, a member, a yes/no; `<filter>_from`? / `<filter>_to`? per date filter \u2014 none for a date holding a span; `<filter>_min`? / `<filter>_max`? per number filter \u2014 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 |\n| `<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 |\n| `<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` |\n| `<entity>_record` | One row of <entity> by its id. | `<entity>_id` | every entity the app draws |\n| `<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 |\n| `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 |\n| `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 |\n| `<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 |\n| `<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`) |\n| `<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 |\n| `<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) \u2014 but the work against its plan (Progress), which reads the list\'s own rows; none over Books |\n| `<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) |\n| `<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) |\n| `<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) |\n| `<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) |\n| `<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 |\n| `<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 |\n| `<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 |\n| `<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 |\n| `<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`) |\n| `<entity>_plan_load` | How many <entity> rows each <place> holds, over every table the register reads \u2014 the rows with no <place> counted apart. | | a register laid out as a `plan`: its places\' counts of present rows |\n\n<!-- generated:end queries -->\n\nA reading\'s query returns groups, never the rows it read: one row for all of them, or one per value it\nsplits by, or per day. Each row carries the alias of each field it splits by, `__day` where it splits by\nday, `__count`, and `__sum` where it sums a number; its description states which. An average is `__sum`\nover `__count`, both over the rows holding a value.\n\nA 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\nlog\'s table before the name: `<entity>_<key>`.\n\nA register reading\'s query reads the rows it names, whatever a screen has open: the rows of its `tab` where\nit states one, every tab\'s where it states none \u2014 its description says which. One placed in time (`over`)\nreads every day unless the call states `from` or `to`.\n\n### Queries the screen runs for itself\n\nThey run like any other, and `get_app` leaves them out: each feeds one control, and takes ids\nthe screen already holds.\n\n<!-- generated:start screen-queries -->\n\n| Called | What it says of itself | Takes | Generated for |\n|---|---|---|---|\n| `<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 |\n| `<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`) |\n| `<entity>_pictures` | The picture and name of each <entity> row among the ids handed in. | `<entity>_ids` | a link to several pictured rows |\n| `<entity>_count_by_<scope>` | How many <entity> rows each <scope entity> holds. | | an app stating `scope` |\n| `<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 |\n| `<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` |\n| `<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` |\n| `<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`) |\n\n<!-- generated:end screen-queries -->\n\n### Workflows\n\n<!-- generated:start workflows -->\n\n| Called | What it says of itself | Takes | Generated for |\n|---|---|---|---|\n| `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 |\n| `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 \u2014 a files field or a link to several rows as `<field>_added` / `<field>_removed` | an entity the app edits |\n| `remove_<entity>` | Removes one row of <entity>. | `record_id` | an entity the app removes rows of |\n| `act_<act>` | <label> \u2014 on one row of <entity>, refused where its conditions do not hold. | `record_id` \u2014 `record_ids` on an act over several rows \u2014 and what the act asks | each Act that names no workflow of its own |\n| `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`) |\n| `<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 |\n\n<!-- generated:end workflows -->\n\nA 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>`.\n\n## Calling a query\n\n```\nget_app {"app_id":"app_\u2026"}\nrun_app_query {"app_id":"app_\u2026","alias":"order_list","params":{"status_is":"open","due_on_to":"2031-03-14"}}\n```\n\n| Argument | Does |\n|---|---|\n| `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|\u2026)` \u2014 a link a record id, a member a member id, a date `yyyy-mm-dd` |\n| `filters` | Narrows the result further, by the query\'s output columns (`filters`) |\n| `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 |\n| `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 |\n| `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` \u2014 a map of name \u2192 filter, never beside `count_by` \u2014 `counts` per name. Never with `fields`, `sort`, `limit` or `cursor` |\n| `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 |\n| `limit`, `cursor` | A page \u2014 200 rows at most, 50 where no `limit` is stated \u2014 and the `next_cursor` of the page before it; `truncated` says more rows matched than `rows` holds |\n| `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 |\n\nA query that states its own `limit` lists at most that many rows: `truncated` is `true` where more match, and `total` is\nthe rows the filtered query matches without it. A `count`, `count_by`, `count_where` and an `aggregate` are over every row\nthe query matches, never over the rows its `limit` lists.\n\nThe rows of one call, across its reads, total at most 262,144 bytes: a read past that is refused, naming\n`fields` and `limit` as what narrows it. The names `filters`, `fields`, `sort`, `count_by` and `aggregate`\ntake are the ones on the query\'s `returns` line.\n\nA list\'s params are how a person narrows that register: `search`, the row\'s id on an app that works inside\none row (`scope`), and a param for each filter the register states that one value or a range narrows. A\nfilter with no param \u2014 on a looked-up link or member, on a number read in each row\'s own unit or currency,\non a date holding a span, or on a register of several tables (Books) \u2014 is narrowed by `filters`, where a\nnumber condition names the unit its value is in as `unit_option`, an option of the figure\'s select by label\nor key. Each tab of a register is a query of its own, taking the list\'s params and the period its rows are\nread over.\n\n## Exporting a file\n\n```\nrun_app_query {"app_id":"app_\u2026","export":{"key":"by_carrier","per":"\u2026","tab":"in_yard","status":["\u2026"]}}\n```\n\n`export` makes the file the register\'s Export button makes, from what the caller reads, and answers it under `files` as a workspace\nfile like any other a tool makes: `file_id`, `url`, `filename`, `mime_type`. `key` is the\nkey of an Export under the register List in `get_app`\'s tree; it is sent without `alias` or any other argument of a\nread. An option is named by its name or its id, as a read\'s filters name it; a member by its id or its name, for a member\nof the app\'s organization; a row, by its id. The other keys\nnarrow the file as the register\'s own controls do, each absent one leaving the register\'s opening view:\n\n| Key | Takes |\n|---|---|\n| `per` | The option, row id or member id the report is made for \u2014 required by an Export that states `per`, refused by one that does not; a member, here and in a member chip, is an id or a name, found as a filter on a member column finds one |\n| `tab`, `period`, `window` | A Tab\'s key (the first when absent); `{ "from", "to" }` days `yyyy-MM-dd` (`yyyy-MM-ddTHH:mm` on a date-and-time field) the tabs read over; a calendar\'s or board\'s days |\n| `status` | Options of the register\'s status that are kept |\n| `filters` | `{ "<field>": <value> }` by the field\'s alias, a value `null` clearing the register\'s opening chip; a value is one of the shapes below |\n| `search`, `overdue`, `waiting` | Words searched, one line each of a pasted list; `true`: only the rows past a deadline; `true`: only the rows waiting on the caller |\n| `sort`, `group`, `folder` | `{ "field", "dir" }`; a field alias; a folder id |\n\n| A `filters` value | For a field that is |\n|---|---|\n| `{ "kind": "ids", "ids": [...], "unset"?: true }` | a select (options) or a link to rows (row ids); `unset` also keeps rows holding none |\n| `{ "kind": "members", "mine": false, "ids": [...], "unset"?: true }` | a member; `mine` is the caller |\n| `{ "kind": "dates", "from": "yyyy-MM-dd" \\| null, "to": \u2026 \\| null, "unset"?: true }` | a date; `null` leaves an end open; `unset` also keeps rows holding none |\n| `{ "kind": "yes_no", "value": true }` | a yes/no |\n| `{ "kind": "number", "min": 1 \\| null, "max": \u2026 \\| null, "unit"?: "\u2026" }` | a number; `unit` the option of a unit or currency field |\n| `{ "kind": "bands", "bands": [{ "min", "max" }] }` | a number the register cuts into tiers |\n\nA key the register lacks, a tab it does not hold or a value its field cannot take is refused, naming it.\n\n## Calling a workflow\n\n```\nrun_app_workflow {"app_id":"app_\u2026","alias":"act_hand_over","inputs":{"record_id":"rec_\u2026"},"dry_run":true}\nrun_app_workflow {"app_id":"app_\u2026","alias":"act_hand_over","inputs":{"record_id":"rec_\u2026"}}\n```\n\n| Argument | Does |\n|---|---|\n| `inputs` | The workflow\'s declared inputs. An update takes the record and only the fields that change |\n| `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 \u2014 or `status: "not_previewed"` where the workflow cannot be dry-run (\xA7 A dry run) |\n| `as_member` | With `dry_run`, for a workspace admin: the run is judged as that member would press it \u2014 their stage owners, row rules and app access \u2014 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 |\n| `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 |\n\n```\nrun_app_workflow {"app_id":"app_\u2026","runs":[\n {"alias":"create_order","inputs":{"customer":"rec_\u2026","item":"rec_\u2026","due_on":"2031-03-14"}},\n {"alias":"act_hand_over","inputs":{"record_id":{"from_run":0,"path":"data.record_id"}}}\n]}\n```\n\n## A dry run\n\nA 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 \u2014 checks, write rules, stage owners \u2014 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`,\n`message`, `field_errors` and `data` as the run would return them, and under `dry_run`:\n\n| Key | Holds |\n|---|---|\n| `available` | `true` |\n| `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 |\n| `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 |\n| `papers` | Each paper rendered: its `step_id` and `filename`. The file is not kept |\n| `unpredicted` | The parts of the answer left out \u2014 `message`, `field_errors`, `data` \u2014 because each reads what a call the dry run did not make would answer |\n\nWhat a write sets off once it commits \u2014 a table\'s own workflows, its events \u2014 does not happen in a dry run.\n\nA workflow is not dry-run where its body reads the answer of a call that leaves the workspace, to branch,\nloop, check or feed another call, where it reads or writes rows after such a call (the run would find them as\nthat call left them), where it runs an agent, where it waits, where a step cannot be evaluated, fails\nfor a reason that is no table\'s refusal, or sends its call an input the call refuses, and where the run is past\nwhat one dry run walks \u2014 its steps, the passes of a loop, 50 reads, 50 writes or 20 seconds, which the runs of\none call share and the database holds it to: a lock wait that outlasts them ends the dry run. The\nanswer is then `status: "not_previewed"` with\n`dry_run: { "available": false, "reason" }`, and nothing ran \u2014 within `runs`, it ends the call. The same\nanswers a caller outside the app\'s organization.\n\n## A reviewed act\n\nAn act that states `review` runs a workflow that only proposes: call it with the inputs the act asks and it answers,\nwriting nothing, the changeset its body proposes in `data.review` \u2014 `table_id`, `creates` (each with its `key`) and\n`updates` (each with its `record_id`), each row\'s `data` holding only the fields that change, by field alias, with an\noptional `note` and `ticked` (`false` where the body leaves it for a person to choose), and `unplaced`, what was read\nand matched nothing; the platform adds a `sha` to every row. Call it again with the same inputs and `approved`,\n`{ "updates": [{ "record_id", "sha" }], "creates": [{ "key", "sha" }] }`, listing the rows to write with the `sha` each\ncarries in the proposal, copied as it came \u2014 never computed \u2014 and the platform writes those rows as that second call\nproposes them, each only where it proposes the values read:\n\n```\nrun_app_workflow {"app_id":"app_\u2026","alias":"match_statement","inputs":{"record_ids":["rec_\u2026"],"statement":"fil_\u2026"}}\nrun_app_workflow {"app_id":"app_\u2026","alias":"match_statement","inputs":{"record_ids":["rec_\u2026"],"statement":"fil_\u2026","approved":{"updates":[{"record_id":"rec_\u2026","sha":"9f2c\u2026"}],"creates":[{"key":"line-4","sha":"41d0\u2026"}]}}}\n```\n\nThe answer\'s `data.applied` names the run (`execution_id`), the records `updated`, what each held before (`previous`), each `created` record by its key,\nand each approved row `left` with its `reason`: `not_proposed` (the second call no longer proposes it), `not_acted`\n(an update of a record the act was not run on), `changed` (the second call proposes other values than were read \u2014 read\nthe proposal again), `empty` (an update changing no field), `missing`, `locked` or `conflict`. Every approved row is\nchecked before any is written, and all are written in one transaction or none; the status history of each row written\nthat 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\nrow in view takes no `record_ids`. A `file` input takes the id of a file uploaded to the workspace.\n\n## A refusal\n\nA workflow re-checks on the server what the screen shows of its own act or save: what an act requires, a\nduplicate, whose step it is, a row the caller may not see. Each write it makes also meets the table\'s own\nrules (a bound, a span, a stage\'s owner, its lock), which hold on every path a record is written by. A row its table\nlocked 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\nin 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\nbefore it writes writes nothing; one whose later write a table\'s rule refuses keeps the writes before it.\nEither answers `status: "error"` with the sentence a person would read as `message`, and under `field_errors`\nthe sentence for each input it was refused over. A value of the wrong type, a missing required input and an unknown\nalias are refused before the body runs: the call itself fails with the sentence, and `field_errors` where\nan input is at fault \u2014 within `runs`, a run refused for its inputs answers `status: "error"` in its place,\nand the runs before it stand. A refused read fails the same way, naming the param or the column; within\n`runs`, it answers its `error` and the other reads are still answered.\n\n## Declaring more\n\nWhat 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\nActs: 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`).\n\n**A query** \u2014 the rows of one table, or one figure of them, in the app\'s own aliases:\n\n```jsonc\n{\n "type": "Query",\n "key": "orders_overdue",\n "description": "A customer\'s orders still open past the day they were due, longest overdue first.",\n "params": {\n "customer": { "type": "record_link", "entity": "customer", "required": false, "description": "Only this customer\'s orders." },\n "as_of": { "type": "date", "description": "The day an order is overdue on: due before it." }\n },\n "rows": "order",\n "fields": ["customer", "item", "due_on", "price"],\n "where": { "status": ["baking"], "customer": { "param": "customer" }, "due_on": { "to": { "param": "as_of" } } },\n "sort": { "field": "due_on" }\n}\n```\n\nIt reads under the table\'s read scope and, on the app\'s own table, exactly the rows its list reads \u2014\nso never the table of an app that states Books, whose list reads several tables. A condition reading an\noptional param narrows nothing when the call leaves it out; no param compares a number read in each row\'s\nown unit or currency, or a date holding a span. `"reading"` in place of `"fields"` returns a count or a sum \u2014\n`{ "value": "price", "by": "item" }` \u2014 as a Reading does. A Query, a Workflow and\na Reading\'s `key` take no name the app generates (`model/app.generated-name`).\n\n**A workflow** \u2014 a write no Act of the record makes, typed in the app\'s tree and its code set on the app:\n\n```jsonc\n{\n "type": "Workflow",\n "key": "import_bookings",\n "description": "Books every order a sheet lists, reusing an order the sheet already booked.",\n "inputs": {\n "sheet": { "type": "file", "description": "The order sheet, one order per row." },\n "booked_by": { "type": "member", "required": false, "description": "Who the orders are taken by; absent, the caller." }\n },\n "outputs": { "added": { "type": "number" }, "reused": { "type": "number" } }\n}\n```\n\n`set_app_workflow` sets its code (`workflows`). A query set on an app outside\nthe tree (`set_app_queries`, on the CLI or in chat) is removed by the next write, whose answer notes it; declared as a Query\nnode, it is the tree\'s. A caller sees the description and\neach input\'s `description` and nothing else, so each says what the value is and what leaving it out means;\nthe check notes one that says nothing.\n\n**Shape each for a caller that is not looking at a screen.** One job a person hands an agent \u2014 a paper read,\na message answered, a sheet brought in \u2014 is one declared workflow named for the job, never a chain of the\ngenerated per-row workflows. Take a list where the job covers many rows, a record\'s lines among them. A workflow\'s code\nthat writes more than one row makes them in one `save_record` workflow step, all or nothing, and recognises\nwhat exists by the identity the input itself carries, declared unique on its table, so the same input sent\ntwice updates instead of adding. Return the ids under `outputs`, so the next run can read them. Gate what\ncannot be undone behind an approval step.\n\n## Prove an app as an agent\n\nRun 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.\n\n1. `get_app` \u2014 every job the app\'s `description` names has an alias that does it or reads it.\n2. `run_app_query` the list with no param, then with each param a person would narrow by; `count: true`\n answers how many without the rows.\n3. `run_app_query` `<entity>_record` with one row\'s id, and each `<child>_by_<link>` under it.\n4. `run_app_workflow` an act with `dry_run: true`, then without.\n5. `run_app_workflow` an act its record is not ready for: the answer is `status: "error"` with the sentence\n the screen would show.\n6. `run_app_workflow` the app\'s job workflow with the input a person would hand it \u2014 the paper read, the row:\n it completes in one call. Call it again with the same input: nothing is added.\n';
1771
+ var agent_surface_default = '# An app\'s agent surface\n\nEverything a person reads or does in an app, an agent reads or does through that app\'s **queries** and\n**workflows** \u2014 the same ones its screen runs, under the same permissions and the same rules. Three tools\nreach them, over MCP and as `lotics run <tool>`:\n\n| Tool | Answers |\n|---|---|\n| `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`) \u2014 beside what each table they name holds, in its owner\'s words |\n| `run_app_query` | The rows, a count or the groups of one query \u2014 or of several in one call |\n| `run_app_workflow` | One write \u2014 or several in order \u2014 or a dry run of it that writes nothing |\n\nAn app generates this surface from its tree; Query and Workflow nodes declare what the screen never\nasks (\xA7 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\nLists and its Acts are queries and workflows of the same app, named by the log\'s table as the register\'s\nare by its own \u2014 `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\n(`app_bindings`).\n\n## Finding what an app offers\n\n`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`.\n\n## What every app generates\n\nA 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\napp has.\n\n### Queries\n\n<!-- generated:start queries -->\n\n| Called | What it says of itself | Takes | Generated for |\n|---|---|---|---|\n| `<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 \u2014 a select, a one-row link, a member, a yes/no; `<filter>_from`? / `<filter>_to`? per date filter \u2014 none for a date holding a span; `<filter>_min`? / `<filter>_max`? per number filter \u2014 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 |\n| `<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 |\n| `<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` |\n| `<entity>_record` | One row of <entity> by its id. | `<entity>_id` | every entity the app draws |\n| `<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 |\n| `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 |\n| `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 |\n| `<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 |\n| `<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`) |\n| `<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 |\n| `<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) \u2014 but the work against its plan (Progress), which reads the list\'s own rows; none over Books |\n| `<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) |\n| `<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) |\n| `<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) |\n| `<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) |\n| `<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 |\n| `<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 |\n| `<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 |\n| `<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 |\n| `<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`) |\n| `<entity>_plan_load` | How many <entity> rows each <place> holds, over every table the register reads \u2014 the rows with no <place> counted apart. | | a register laid out as a `plan`: its places\' counts of present rows |\n\n<!-- generated:end queries -->\n\nA reading\'s query returns groups, never the rows it read: one row for all of them, or one per value it\nsplits by, or per day. Each row carries the alias of each field it splits by, `__day` where it splits by\nday, `__count`, and `__sum` where it sums a number; its description states which. An average is `__sum`\nover `__count`, both over the rows holding a value.\n\nA Reading is called by its `key` (`reference/reading-node`), so moving it in the tree changes no name. A log\'s Reading is called by the\nlog\'s table before the name: `<entity>_<key>`.\n\nA register reading\'s query reads the rows it names, whatever a screen has open: the rows of its `tab` where\nit states one, every tab\'s where it states none \u2014 its description says which. One placed in time (`over`)\nreads every day unless the call states `from` or `to`.\n\n### Queries the screen runs for itself\n\nThey run like any other, and `get_app` leaves them out: each feeds one control, and takes ids\nthe screen already holds.\n\n<!-- generated:start screen-queries -->\n\n| Called | What it says of itself | Takes | Generated for |\n|---|---|---|---|\n| `<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 |\n| `<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`) |\n| `<entity>_pictures` | The picture and name of each <entity> row among the ids handed in. | `<entity>_ids` | a link to several pictured rows |\n| `<entity>_count_by_<scope>` | How many <entity> rows each <scope entity> holds. | | an app stating `scope` |\n| `<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 |\n| `<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` |\n| `<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` |\n| `<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`) |\n\n<!-- generated:end screen-queries -->\n\n### Workflows\n\n<!-- generated:start workflows -->\n\n| Called | What it says of itself | Takes | Generated for |\n|---|---|---|---|\n| `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 |\n| `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 \u2014 a files field or a link to several rows as `<field>_added` / `<field>_removed` | an entity the app edits |\n| `remove_<entity>` | Removes one row of <entity>. | `record_id` | an entity the app removes rows of |\n| `act_<act>` | <label> \u2014 on one row of <entity>, refused where its conditions do not hold. | `record_id` \u2014 `record_ids` on an act over several rows \u2014 and what the act asks | each Act that names no workflow of its own |\n| `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`) |\n| `<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 |\n\n<!-- generated:end workflows -->\n\nA 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>`.\n\n## Calling a query\n\n```\nget_app {"app_id":"app_\u2026"}\nrun_app_query {"app_id":"app_\u2026","alias":"order_list","params":{"status_is":"open","due_on_to":"2031-03-14"}}\n```\n\n| Argument | Does |\n|---|---|\n| `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|\u2026)` \u2014 a link a record id, a member a member id, a date `yyyy-mm-dd` |\n| `filters` | Narrows the result further, by the query\'s output columns (`filters`) |\n| `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 |\n| `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 |\n| `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` \u2014 a map of name \u2192 filter, never beside `count_by` \u2014 `counts` per name. Never with `fields`, `sort`, `limit` or `cursor` |\n| `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 |\n| `limit`, `cursor` | A page \u2014 200 rows at most, 50 where no `limit` is stated \u2014 and the `next_cursor` of the page before it; `truncated` says more rows matched than `rows` holds |\n| `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 |\n\nA query that states its own `limit` lists at most that many rows: `truncated` is `true` where more match, and `total` is\nthe rows the filtered query matches without it. A `count`, `count_by`, `count_where` and an `aggregate` are over every row\nthe query matches, never over the rows its `limit` lists.\n\nThe rows of one call, across its reads, total at most 262,144 bytes: a read past that is refused, naming\n`fields` and `limit` as what narrows it. The names `filters`, `fields`, `sort`, `count_by` and `aggregate`\ntake are the ones on the query\'s `returns` line.\n\nA list\'s params are how a person narrows that register: `search`, the row\'s id on an app that works inside\none row (`scope`), and a param for each filter the register states that one value or a range narrows. A\nfilter with no param \u2014 on a looked-up link or member, on a number read in each row\'s own unit or currency,\non a date holding a span, or on a register of several tables (Books) \u2014 is narrowed by `filters`, where a\nnumber condition names the unit its value is in as `unit_option`, an option of the figure\'s select by label\nor key. Each tab of a register is a query of its own, taking the list\'s params and the period its rows are\nread over.\n\n## Exporting a file\n\n```\nrun_app_query {"app_id":"app_\u2026","export":{"key":"by_carrier","per":"\u2026","tab":"in_yard","status":["\u2026"]}}\n```\n\n`export` makes the file the register\'s Export button makes, from what the caller reads, and answers it under `files` as a workspace\nfile like any other a tool makes: `file_id`, `url`, `filename`, `mime_type`. `key` is the\nkey of an Export under the register List in `get_app`\'s tree; it is sent without `alias` or any other argument of a\nread. An option is named by its name or its id, as a read\'s filters name it; a member by its id or its name, for a member\nof the app\'s organization; a row, by its id. The other keys\nnarrow the file as the register\'s own controls do, each absent one leaving the register\'s opening view:\n\n| Key | Takes |\n|---|---|\n| `per` | The option, row id or member id the report is made for \u2014 required by an Export that states `per`, refused by one that does not; a member, here and in a member chip, is an id or a name, found as a filter on a member column finds one |\n| `tab`, `period`, `window` | A Tab\'s key (the first when absent); `{ "from", "to" }` days `yyyy-MM-dd` (`yyyy-MM-ddTHH:mm` on a date-and-time field) the tabs read over; a calendar\'s or board\'s days |\n| `status` | Options of the register\'s status that are kept |\n| `filters` | `{ "<field>": <value> }` by the field\'s alias, a value `null` clearing the register\'s opening chip; a value is one of the shapes below |\n| `search`, `overdue`, `waiting` | Words searched, one line each of a pasted list; `true`: only the rows past a deadline; `true`: only the rows waiting on the caller |\n| `sort`, `group`, `folder` | `{ "field", "dir" }`; a field alias; a folder id |\n\n| A `filters` value | For a field that is |\n|---|---|\n| `{ "kind": "ids", "ids": [...], "unset"?: true }` | a select (options) or a link to rows (row ids); `unset` also keeps rows holding none |\n| `{ "kind": "members", "mine": false, "ids": [...], "unset"?: true }` | a member; `mine` is the caller |\n| `{ "kind": "dates", "from": "yyyy-MM-dd" \\| null, "to": \u2026 \\| null, "unset"?: true }` | a date; `null` leaves an end open; `unset` also keeps rows holding none |\n| `{ "kind": "yes_no", "value": true }` | a yes/no |\n| `{ "kind": "number", "min": 1 \\| null, "max": \u2026 \\| null, "unit"?: "\u2026" }` | a number; `unit` the option of a unit or currency field |\n| `{ "kind": "bands", "bands": [{ "min", "max" }] }` | a number the register cuts into tiers |\n\nA key the register lacks, a tab it does not hold or a value its field cannot take is refused, naming it.\n\n## Calling a workflow\n\n```\nrun_app_workflow {"app_id":"app_\u2026","alias":"act_hand_over","inputs":{"record_id":"rec_\u2026"},"dry_run":true}\nrun_app_workflow {"app_id":"app_\u2026","alias":"act_hand_over","inputs":{"record_id":"rec_\u2026"}}\n```\n\n| Argument | Does |\n|---|---|\n| `inputs` | The workflow\'s declared inputs. An update takes the record and only the fields that change |\n| `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 \u2014 or `status: "not_previewed"` where the workflow cannot be dry-run (\xA7 A dry run) |\n| `as_member` | With `dry_run`, for a workspace admin: the run is judged as that member would press it \u2014 their stage owners, row rules and app access \u2014 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 |\n| `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 |\n\n```\nrun_app_workflow {"app_id":"app_\u2026","runs":[\n {"alias":"create_order","inputs":{"customer":"rec_\u2026","item":"rec_\u2026","due_on":"2031-03-14"}},\n {"alias":"act_hand_over","inputs":{"record_id":{"from_run":0,"path":"data.record_id"}}}\n]}\n```\n\n## A dry run\n\nA 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 \u2014 checks, write rules, stage owners \u2014 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`,\n`message`, `field_errors` and `data` as the run would return them, and under `dry_run`:\n\n| Key | Holds |\n|---|---|\n| `available` | `true` |\n| `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 |\n| `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 |\n| `papers` | Each paper rendered: its `step_id` and `filename`. The file is not kept |\n| `unpredicted` | The parts of the answer left out \u2014 `message`, `field_errors`, `data` \u2014 because each reads what a call the dry run did not make would answer |\n\nWhat a write sets off once it commits \u2014 a table\'s own workflows, its events \u2014 does not happen in a dry run.\n\nA workflow is not dry-run where its body reads the answer of a call that leaves the workspace, to branch,\nloop, check or feed another call, where it reads or writes rows after such a call (the run would find them as\nthat call left them), where it runs an agent, where it waits, where a step cannot be evaluated, fails\nfor a reason that is no table\'s refusal, or sends its call an input the call refuses, and where the run is past\nwhat one dry run walks \u2014 its steps, the passes of a loop, 50 reads, 50 writes or 20 seconds, which the runs of\none call share and the database holds it to: a lock wait that outlasts them ends the dry run. The\nanswer is then `status: "not_previewed"` with\n`dry_run: { "available": false, "reason" }`, and nothing ran \u2014 within `runs`, it ends the call. The same\nanswers a caller outside the app\'s organization.\n\n## A reviewed act\n\nAn act that states `review` runs a workflow that only proposes: call it with the inputs the act asks and it answers,\nwriting nothing, the changeset its body proposes in `data.review` \u2014 `table_id`, `creates` (each with its `key`) and\n`updates` (each with its `record_id`), each row\'s `data` holding only the fields that change, by field alias, with an\noptional `note` and `ticked` (`false` where the body leaves it for a person to choose), and `unplaced`, what was read\nand matched nothing; the platform adds a `sha` to every row. Call it again with the same inputs and `approved`,\n`{ "updates": [{ "record_id", "sha" }], "creates": [{ "key", "sha" }] }`, listing the rows to write with the `sha` each\ncarries in the proposal, copied as it came \u2014 never computed \u2014 and the platform writes those rows as that second call\nproposes them, each only where it proposes the values read:\n\n```\nrun_app_workflow {"app_id":"app_\u2026","alias":"match_statement","inputs":{"record_ids":["rec_\u2026"],"statement":"fil_\u2026"}}\nrun_app_workflow {"app_id":"app_\u2026","alias":"match_statement","inputs":{"record_ids":["rec_\u2026"],"statement":"fil_\u2026","approved":{"updates":[{"record_id":"rec_\u2026","sha":"9f2c\u2026"}],"creates":[{"key":"line-4","sha":"41d0\u2026"}]}}}\n```\n\nThe answer\'s `data.applied` names the run (`execution_id`), the records `updated`, what each held before (`previous`), each `created` record by its key,\nand each approved row `left` with its `reason`: `not_proposed` (the second call no longer proposes it), `not_acted`\n(an update of a record the act was not run on), `changed` (the second call proposes other values than were read \u2014 read\nthe proposal again), `empty` (an update changing no field), `missing`, `locked` or `conflict`. Every approved row is\nchecked before any is written, and all are written in one transaction or none; the status history of each row written\nthat 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\nrow in view takes no `record_ids`. A `file` input takes the id of a file uploaded to the workspace.\n\n## A refusal\n\nA workflow re-checks on the server what the screen shows of its own act or save: what an act requires, a\nduplicate, whose step it is, a row the caller may not see. Each write it makes also meets the table\'s own\nrules (a bound, a span, a stage\'s owner, its lock), which hold on every path a record is written by. A row its table\nlocked 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\nin 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\nbefore it writes writes nothing; one whose later write a table\'s rule refuses keeps the writes before it.\nEither answers `status: "error"` with the sentence a person would read as `message`, and under `field_errors`\nthe sentence for each input it was refused over. A value of the wrong type, a missing required input and an unknown\nalias are refused before the body runs: the call itself fails with the sentence, and `field_errors` where\nan input is at fault \u2014 within `runs`, a run refused for its inputs answers `status: "error"` in its place,\nand the runs before it stand. A refused read fails the same way, naming the param or the column; within\n`runs`, it answers its `error` and the other reads are still answered.\n\n## Declaring more\n\nWhat 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\nActs: 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`; `reference/query-node`, `reference/workflow-node`).\n\n**A query** \u2014 the rows of one table, or one figure of them, in the app\'s own aliases:\n\n```jsonc\n{\n "type": "Query",\n "key": "orders_overdue",\n "description": "A customer\'s orders still open past the day they were due, longest overdue first.",\n "params": {\n "customer": { "type": "record_link", "entity": "customer", "required": false, "description": "Only this customer\'s orders." },\n "as_of": { "type": "date", "description": "The day an order is overdue on: due before it." }\n },\n "rows": "order",\n "fields": ["customer", "item", "due_on", "price"],\n "where": { "status": ["baking"], "customer": { "param": "customer" }, "due_on": { "to": { "param": "as_of" } } },\n "sort": { "field": "due_on" }\n}\n```\n\nIt reads under the table\'s read scope and, on the app\'s own table, exactly the rows its list reads \u2014\nso never the table of an app that states Books, whose list reads several tables. A condition reading an\noptional param narrows nothing when the call leaves it out; no param compares a number read in each row\'s\nown unit or currency, or a date holding a span. `"reading"` in place of `"fields"` returns a count or a sum \u2014\n`{ "value": "price", "by": "item" }` \u2014 as a Reading does. A Query, a Workflow and\na Reading\'s `key` take no name the app generates (`reference/app.generated-name`).\n\n**A workflow** \u2014 a write no Act of the record makes, typed in the app\'s tree and its code set on the app:\n\n```jsonc\n{\n "type": "Workflow",\n "key": "import_bookings",\n "description": "Books every order a sheet lists, reusing an order the sheet already booked.",\n "inputs": {\n "sheet": { "type": "file", "description": "The order sheet, one order per row." },\n "booked_by": { "type": "member", "required": false, "description": "Who the orders are taken by; absent, the caller." }\n },\n "outputs": { "added": { "type": "number" }, "reused": { "type": "number" } }\n}\n```\n\n`set_app_workflow` sets its code (`workflows`). A query set on an app outside\nthe tree (`set_app_queries`, on the CLI or in chat) is removed by the next write, whose answer notes it; declared as a Query\nnode, it is the tree\'s. A caller sees the description and\neach input\'s `description` and nothing else, so each says what the value is and what leaving it out means;\nthe check notes one that says nothing.\n\n**Shape each for a caller that is not looking at a screen.** One job a person hands an agent \u2014 a paper read,\na message answered, a sheet brought in \u2014 is one declared workflow named for the job, never a chain of the\ngenerated per-row workflows. Take a list where the job covers many rows, a record\'s lines among them. A workflow\'s code\nthat writes more than one row makes them in one `save_record` workflow step, all or nothing, and recognises\nwhat exists by the identity the input itself carries, declared unique on its table, so the same input sent\ntwice updates instead of adding. Return the ids under `outputs`, so the next run can read them. Gate what\ncannot be undone behind an approval step.\n\n## Prove an app as an agent\n\nRun 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.\n\n1. `get_app` \u2014 every job the app\'s `description` names has an alias that does it or reads it.\n2. `run_app_query` the list with no param, then with each param a person would narrow by; `count: true`\n answers how many without the rows.\n3. `run_app_query` `<entity>_record` with one row\'s id, and each `<child>_by_<link>` under it.\n4. `run_app_workflow` an act with `dry_run: true`, then without.\n5. `run_app_workflow` an act its record is not ready for: the answer is `status: "error"` with the sentence\n the screen would show.\n6. `run_app_workflow` the app\'s job workflow with the input a person would hand it \u2014 the paper read, the row:\n it completes in one call. Call it again with the same input: nothing is added.\n';
1772
1772
 
1773
1773
  // AGENTS.md
1774
- var AGENTS_default = "# @lotics/cli \u2014 agent index\n\nThe model an agent needs before driving this CLI: which surface answers which question, what the\nconventions are, and where the traps are.\n\n| Read | For |\n|---|---|\n| `lotics --help` | The verb inventory (\xA7 COMMANDS) and global flags. The verb LIST is generated and never stale; the prose beside each verb is hand-written, so where it disagrees with `docs/cli_reference.md`, the reference wins. |\n| `lotics tools` \xB7 `lotics tools <name>` | The agent tool registry and one tool's full JSON Schema. |\n| `lotics docs` \xB7 `lotics docs <area>[/<section>]` \xB7 `lotics docs [<area>] --grep <text>` | This CLI's references, carried inside it so each describes the binary answering. Capped at a page: a doc that does not fit hands back its opening and the addresses into it, so the next call is smaller than the last. A page the copy lacks \u2014 one the server added since this binary was built, which a refusal can cite \u2014 is read from the server's `docs` tool with this machine's credential, and so is every `--grep`, over the references the server serves. A custom-code app's SDK reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the app. |\n| `lotics docs model` \xB7 `lotics docs model/<section>` | The keys of a table document and the components of an app tree: a table's fields keyed by alias, how a row of each table is recognised (`records`), and the apps stated over them. Every key, every field type with the config it needs, the rules, and a worked example; example files of several trades are listed at `https://lotics.ai/presets/index.json` \u2014 read one and pass its tables to `set_tables`. |\n| `lotics docs design` \xB7 `lotics docs design/<section>` | Designing an app \u2014 the treatment each kind of row takes with the `model` page stating its keys, the visual bar, and looking at what a write built. Opens on the common treatments; a finding names its section. |\n| `lotics docs examples` \xB7 `lotics docs examples/<treatment>` | Each treatment worked through in an app of a complete workspace: the job, the props to read, where it is the wrong one, and the excerpt. |\n| [docs/getting_started.md](./docs/getting_started.md) | What a workspace holds, and the one page to open for each job: building a workspace, changing an app, a rule on a table, a document, data and questions, a connected service, sharing. The first page an agent outside the product reads. |\n| [docs/first_build.md](./docs/first_build.md) | A whole workspace built call by call \u2014 understand the work, tables with a check, the paper, the app (which apps, a treatment per kind of row, what the check refuses, who gets it), the owner's rows, pressing it \u2014 in one worked example. |\n| [docs/app_architecture.md](./docs/app_architecture.md) | One factory's split into apps: each app's job sentence, person, register and what sits inside it. The questions that split a business are in first_build. |\n| [docs/working_in_a_workspace.md](./docs/working_in_a_workspace.md) | The right means for each job once the apps exist: an app's act for a business operation, a record write for data entry, a query for a figure the business defines, a count for the rest. |\n| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE \u2014 clarify, tables, apps, prove, look \u2014 for an app stated as a tree and for a custom-code app. Read it once before starting an app. Looking is `lotics run screenshot_app '<json>' -o <dir>`, which saves each PNG and its text snapshot there. |\n| [docs/cli_reference.md](./docs/cli_reference.md) | Per-command contracts, flags, exit codes, and gotchas \u2014 the detail `--help` compresses. |\n| [docs/data_model.md](./docs/data_model.md) | Tables, their fields, and how they relate \u2014 one fact per column, one table per kind of thing and the NAME-OVERLAP probe that says when a split has broken, one vocabulary wherever values are copied, a copy boundary that accounts for every source field, provenance as a link, a declared natural key, history as rows, and why derived DEPTH costs more than row count; then the choices around a field's type, and how a field is changed (a list replaces whole, what refuses a deletion, what `force` consents to). Separate from building_an_app because every workspace starts with tables and many never get an app. |\n| [docs/filters.md](./docs/filters.md) | The one filter grammar every filter-taking tool, view and rollup reads \u2014 conditions, groups, and the operators per field type. |\n| [docs/field_values.md](./docs/field_values.md) | The value each field type takes in a write. |\n| [docs/workflows.md](./docs/workflows.md) | Writing a workflow body \u2014 triggers, steps, what an expression reads, the helpers, approvals and agent steps, and table checks. |\n| [docs/app_bindings.md](./docs/app_bindings.md) | The queries, workflows and agents an app binds \u2014 the query tree, typed inputs and outputs, and an agent's declaration. |\n| [docs/document_templates.md](./docs/document_templates.md) | Generating PDF/Excel/Word/email from reusable templates, and what a template an app's Act makes is handed. |\n| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the procedures, policies and glossary an agent can't guess with `set_knowledge`; who can read a doc; searching and reading it line by line. |\n| [docs/migration.md](./docs/migration.md) | What to DO when something an earlier CLI wrote to disk no longer matches what it does \u2014 local app projects are gone, and each verb that read one has a replacement. |\n| [README.md](./README.md) | Install, auth, and worked examples. |\n\n## Two surfaces, and the trap between them\n\n**`lotics tools` is not the CLI's capability list.** There are two disjoint surfaces:\n\n- **Tools** (`lotics tools`, `lotics run <tool>`) \u2014 the *agent tool registry*: what an agent, workflow,\n or automation may call. Workspace data, templates, knowledge, admin.\n- **Commands** (`lotics --help` \xA7 COMMANDS) \u2014 the CLI's *own verbs*: auth and org/workspace scoping,\n file upload and download, and those that touch a local file for an app \u2014 `app apply`,\n `app create --custom`, `app pull` and `app deploy`.\n\nSeveral capabilities exist **only** as commands and appear nowhere in `lotics tools` \u2014 downloading a\nfile is the one most often mistaken for missing. Concluding \"the platform can't do X\" from the tool\nlist alone is a mistake; check both.\n\n**`lotics <verb> --help` prints that verb's entries from \xA7 COMMANDS** (`lotics app --help`,\n`lotics file download --help`); `lotics report --help` prints the report frame. To find out whether\nsomething exists, read `lotics --help` \xA7 COMMANDS \u2014 the whole section, not a narrow grep.\n\n## Conventions that hold across every command\n\n- **Scope is resolved per invocation.** `LOTICS_ORG` / `LOTICS_WORKSPACE` (or `LOTICS_API_KEY`) scope a\n single call without changing the active org or a directory pin \u2014 the safe way to touch one tenant\n from a shell serving many. Every command that resolves a workspace echoes its target to **stderr**\n (`lotics \u2192 <org> / <workspace>`); read it back before trusting a write. Resolution precedence is\n in README \xA7 Organizations.\n- **A machine with no key can still sign in, and the sign-in never blocks you.** `lotics auth\n login <email>` prints the page a person opens (also mailed) and the code that page must show,\n records the request, and EXITS. They press Confirm whenever they get to it; the next command that\n NEEDS a credential collects the key before doing its own work, so \"sign in\" costs you one command\n and then re-running what you wanted. Never wrap it in a timeout waiting for a human \u2014 `--wait`\n exists if you really want one blocking command, and killing that one is safe too (the request\n survives and the next command claims it). A command run before Confirm exits 1 naming the page and\n code again; once the 15 minutes are up it says to ask again. `lotics auth signup <email>` for an email that\n already has an account creates nothing and names `auth login`.\n- **A credential is either a SIGN-IN or an API KEY, and `logout` treats them differently.** A profile\n from `auth login` / `auth signup` acts as the person who confirmed it and is theirs \u2014 `lotics auth\n logout` revokes it server-side, and it lapses on its own after 90 idle days (each use pushes that\n out). A profile from `auth api-key` holds a key an ADMIN issued. A key created in Settings carries\n its OWN access \u2014 every app and table, or only the ones chosen on the key, so a listing that comes\n back short is the key's reach, not a bug \u2014 while a key created FOR a person carries that\n person's access and dies with their membership. Either is routinely also on a server and on\n other machines, so logout only forgets it locally and says so; only an admin revokes it. A profile that states no kind (saved before the field existed) is resolved against the SERVER\n and revoked only if the answer is a sign-in; a bare `--api-key` / `LOTICS_API_KEY` names no\n profile to remove at all. Nothing is ever revoked on a guess \u2014 between two, the destructive one is\n wrong. `lotics auth whoami` prints the kind, asking the server when the store cannot say.\n- **A key created in Settings never administers the organization, whatever its access.** The verbs\n `docs/cli_reference.md` marks *admin only* split in two under a key: `workspace doctor`, which only\n reads inside a workspace the key reaches, runs as before, while managing people, sharing or\n ownership, creating or deleting a workspace, changing workspace settings, setting credit limits,\n reading the access log and publishing an app's API answer `403` and name the remedy: an admin signed in, so\n `lotics auth login <email>` and run it again. A sign-in acts as that person and is refused none of\n them. Do not retry a `403` with the same credential and do not ask for a wider key \u2014 no answer on\n the key's own screen grants this.\n- **A 401 names its remedy \u2014 act on the hint, do not retry.** \"This credential expired / was\n revoked / belongs to a member who is no longer active\" carries the one remedy that ends this\n credential: run `lotics auth login <email>` for a sign-in, or ask the admin who issued it for a\n new key. A credential minted before that was recorded carries BOTH, because nothing on the row\n tells them apart \u2014 so on a box with no browser, take the second. Only an UNRECOGNIZED key gets\n the generic \"Invalid or disabled API key\", and that one is generic on purpose, so re-sending it\n teaches nothing. A `reason` rides on the body for a script to branch on, since the code stays\n `unauthorized` for every 401.\n- **Large payloads bypass `ARG_MAX`** \u2014 `lotics run <tool> @args.json`, or piped stdin behind `-`. A leading `@` is\n unambiguously a file path (JSON args start with `{`).\n- **stdout is the payload, stderr is the narration.** Progress, status lines, and the target echo go to\n stderr; the result goes to stdout, so piping stays clean. `--json` switches stdout from the\n agent-readable summary to the full structured object.\n- **Every tool is invoked one way \u2014 `lotics run <tool>`.** Including the ones that RUN something\n (`run_app_workflow`, `run_app_query`) and every one that builds\n (`set_tables`, `set_app`, `set_app_queries`, `set_app_workflow`). A command\n exists only for work that touches a local file.\n- **Building is two tools, each a writer that creates without an id and edits with one.** `set_tables`\n takes `{ \"tables\": [...] }` \u2014 always a list, one or many, all or nothing \u2014 each table a document keyed\n by alias (fields keyed by alias; a link names its table by id, or by the `ref` of another table of the\n same call, and states `multi`; a two-way link states `pair` on both tables), edited by an RFC 7396\n merge patch of that document, where `null` deletes. A call carries at most 50 tables, 200 first rows a table,\n and 2000 rows and 2000 documents in all. `set_app` takes ONE app: `{ \"body\": <the App tree> }` to create, the app's\n `name`, `description`, `icon` and `color` being props of the App node; `{ \"app_id\": \u2026, \"nodes\": { \"<address>\": <patch> } }`\n to change a node, refused with `base_version_id` when the app has moved past the version you read. `dry_run` checks\n without writing, a refusal lists everything wrong in the call, and every answer states the permissions the write\n granted and removed. The answer notes the treatments an author leaves unstated; nothing is written into the tree for them. A new workspace: one `set_tables` call with\n the tables, then every `set_app` call, then fixes. The example files under `https://lotics.ai/presets/index.json` are read and their tables passed to\n `set_tables`.\n- **Every change to an app mints a version of it, and applying the earlier tree again is the undo.** An apply, a\n deploy, a single `set_app_*` or `remove_app_binding`: each is a new version. Applying the earlier tree again restores the app \u2014 never a table change or a row a workflow wrote,\n so try a write on a throwaway record.\n- **Exit codes are assertable, and they report the WORK rather than the call.** `lotics run` exits\n non-zero when a `run_app_workflow` result's own envelope carries a failed\n `status` (`error`/`failed`/`cancelled`, or `not_previewed`: a dry run asked for and not given) \u2014 any other tool's top-level status is data and exits 0 \u2014\n so `lotics run \u2026 && next-step` cannot walk past a refused run; `workspace doctor` exits non-zero on\n findings. An unrecognized status exits 0 \u2014 the list is an allowlist of failure, so a status added\n later never turns a working script red \u2014 and a parked run (`awaiting_input`) is not a failure.\n- **An app that PUBLISHES an API turns every later binding write into a release.** Publishing\n snapshots what the app's queries, workflows and agents promise to callers outside it \u2014 a\n customer's own site or server, which nobody here can redeploy. From then on an additive change\n re-snapshots silently and a breaking one is REFUSED, naming each change; the tool's\n `acknowledge_breaking_api_change` carries it out and snapshots the break as a new contract version.\n- **Exposure is per app, all or nothing** \u2014 a public share or a key reaches every alias an app\n declares, so what outsiders may call is a second app over the same tables\n ([docs/building_an_app.md](./docs/building_an_app.md) \xA7 7).\n- **`--print-created` / `--cleanup` on any call that reports `side_effects`.** The first prints the\n records created plus a paste-ready cleanup plan and what cannot be auto-undone; the second runs\n those deletes (records only \u2014 never files, integrations or notifications). Neither is a rollback.\n- **Text output is the default and is built for reading**; reach for `--json` only when a field is\n needed programmatically.\n- **`lotics report '<json>'` is the channel for what nothing else records.** Reach for it\n when the platform is genuinely missing something (a capability that does not exist \u2014 no\n command ran), when a success was wrong (exited 0, wrong effect), or when an error did\n not name the remedy \u2014 not for your own mistakes, which the logs already show.\n **It is a frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, because a log\n reconstructs what you RAN and never what you WANTED, and that gap is the report. `goal` and\n `actual` are required; there is no severity or category to pick. The\n session's commands attach themselves \u2014 do not retype them. Run it bare for the full prompt; a\n long one rides `@file.json` or an explicit `-` for stdin (bare NEVER reads stdin).\n- **`LOTICS_TELEMETRY=1` correlates a whole session** so the authoring loop's rough edges can be\n found and fixed. Off by default; set it in the shell profile, not per command (each invocation is\n its own process). It sends no arguments, no file contents, and no record data \u2014 see README\n \xA7 Diagnostics.\n\n## Where this CLI is not the answer\n\n- **Editing an app in a local directory.** An app stated as a tree is changed by applying the\n tree again; its bindings through their `set_app_*` tools; a custom-code app's code by deploying\n its directory again. Nothing reads a local copy of a live app back.\n- **OAuth connections.** Attaching a connected account is web-only; the CLI can list them.\n";
1774
+ var AGENTS_default = "# @lotics/cli \u2014 agent index\n\nThe model an agent needs before driving this CLI: which surface answers which question, what the\nconventions are, and where the traps are.\n\n| Read | For |\n|---|---|\n| `lotics --help` | The verb inventory (\xA7 COMMANDS) and global flags. The verb LIST is generated and never stale; the prose beside each verb is hand-written, so where it disagrees with `docs/cli_reference.md`, the reference wins. |\n| `lotics tools` \xB7 `lotics tools <name>` | The agent tool registry and one tool's full JSON Schema. |\n| `lotics docs` \xB7 `lotics docs <area>[/<section>]` \xB7 `lotics docs [<area>] --grep <text>` | This CLI's references, carried inside it so each describes the binary answering. Capped at a page: a doc that does not fit hands back its opening and the addresses into it, so the next call is smaller than the last. A page the copy lacks \u2014 one the server added since this binary was built, which a refusal can cite \u2014 is read from the server's `docs` tool with this machine's credential, and so is every `--grep`, over the references the server serves. A custom-code app's SDK reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the app. |\n| `lotics docs reference` \xB7 `lotics docs reference/<section>` | The keys of a table document and the components of an app tree: a table's fields keyed by alias, how a row of each table is recognised (`records`), and the apps stated over them. Every key, every field type with the config it needs, the rules, and a worked example; example files of several trades are listed at `https://lotics.ai/presets/index.json` \u2014 read one and pass its tables to `set_tables`. |\n| `lotics docs design` \xB7 `lotics docs design/<section>` | Designing an app \u2014 the treatment each kind of row takes with the `model` page stating its keys, the visual bar, and looking at what a write built. Opens on the common treatments; a finding names its section. |\n| `lotics docs examples` \xB7 `lotics docs examples/<treatment>` | Each treatment worked through in an app of a complete workspace: the job, the props to read, where it is the wrong one, and the excerpt. |\n| [docs/getting_started.md](./docs/getting_started.md) | What a workspace holds, and the one page to open for each job: building a workspace, changing an app, a rule on a table, a document, data and questions, a connected service, sharing. The first page an agent outside the product reads. |\n| [docs/first_build.md](./docs/first_build.md) | A whole workspace built call by call \u2014 understand the work, tables with a check, the paper, the app (which apps, a treatment per kind of row, what the check refuses, who gets it), the owner's rows, pressing it \u2014 in one worked example. |\n| [docs/app_architecture.md](./docs/app_architecture.md) | One factory's split into apps: each app's job sentence, person, register and what sits inside it. The questions that split a business are in first_build. |\n| [docs/working_in_a_workspace.md](./docs/working_in_a_workspace.md) | The right means for each job once the apps exist: an app's act for a business operation, a record write for data entry, a query for a figure the business defines, a count for the rest. |\n| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE \u2014 clarify, tables, apps, prove, look \u2014 for an app stated as a tree and for a custom-code app. Read it once before starting an app. Looking is `lotics run screenshot_app '<json>' -o <dir>`, which saves each PNG and its text snapshot there. |\n| [docs/cli_reference.md](./docs/cli_reference.md) | Per-command contracts, flags, exit codes, and gotchas \u2014 the detail `--help` compresses. |\n| [docs/data_model.md](./docs/data_model.md) | Tables, their fields, and how they relate \u2014 one fact per column, one table per kind of thing and the NAME-OVERLAP probe that says when a split has broken, one vocabulary wherever values are copied, a copy boundary that accounts for every source field, provenance as a link, a declared natural key, history as rows, and why derived DEPTH costs more than row count; then the choices around a field's type, and how a field is changed (a list replaces whole, what refuses a deletion, what `force` consents to). Separate from building_an_app because every workspace starts with tables and many never get an app. |\n| [docs/filters.md](./docs/filters.md) | The one filter grammar every filter-taking tool, view and rollup reads \u2014 conditions, groups, and the operators per field type. |\n| [docs/field_values.md](./docs/field_values.md) | The value each field type takes in a write. |\n| [docs/workflows.md](./docs/workflows.md) | Writing a workflow body \u2014 triggers, steps, what an expression reads, the helpers, approvals and agent steps, and table checks. |\n| [docs/app_bindings.md](./docs/app_bindings.md) | The queries, workflows and agents an app binds \u2014 the query tree, typed inputs and outputs, and an agent's declaration. |\n| [docs/document_templates.md](./docs/document_templates.md) | Generating PDF/Excel/Word/email from reusable templates, and what a template an app's Act makes is handed. |\n| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the procedures, policies and glossary an agent can't guess with `set_knowledge`; who can read a doc; searching and reading it line by line. |\n| [docs/migration.md](./docs/migration.md) | What to DO when something an earlier CLI wrote to disk no longer matches what it does \u2014 local app projects are gone, and each verb that read one has a replacement. |\n| [README.md](./README.md) | Install, auth, and worked examples. |\n\n## Two surfaces, and the trap between them\n\n**`lotics tools` is not the CLI's capability list.** There are two disjoint surfaces:\n\n- **Tools** (`lotics tools`, `lotics run <tool>`) \u2014 the *agent tool registry*: what an agent, workflow,\n or automation may call. Workspace data, templates, knowledge, admin.\n- **Commands** (`lotics --help` \xA7 COMMANDS) \u2014 the CLI's *own verbs*: auth and org/workspace scoping,\n file upload and download, and those that touch a local file for an app \u2014 `app apply`,\n `app create --custom`, `app pull` and `app deploy`.\n\nSeveral capabilities exist **only** as commands and appear nowhere in `lotics tools` \u2014 downloading a\nfile is the one most often mistaken for missing. Concluding \"the platform can't do X\" from the tool\nlist alone is a mistake; check both.\n\n**`lotics <verb> --help` prints that verb's entries from \xA7 COMMANDS** (`lotics app --help`,\n`lotics file download --help`); `lotics report --help` prints the report frame. To find out whether\nsomething exists, read `lotics --help` \xA7 COMMANDS \u2014 the whole section, not a narrow grep.\n\n## Conventions that hold across every command\n\n- **Scope is resolved per invocation.** `LOTICS_ORG` / `LOTICS_WORKSPACE` (or `LOTICS_API_KEY`) scope a\n single call without changing the active org or a directory pin \u2014 the safe way to touch one tenant\n from a shell serving many. Every command that resolves a workspace echoes its target to **stderr**\n (`lotics \u2192 <org> / <workspace>`); read it back before trusting a write. Resolution precedence is\n in README \xA7 Organizations.\n- **A machine with no key can still sign in, and the sign-in never blocks you.** `lotics auth\n login <email>` prints the page a person opens (also mailed) and the code that page must show,\n records the request, and EXITS. They press Confirm whenever they get to it; the next command that\n NEEDS a credential collects the key before doing its own work, so \"sign in\" costs you one command\n and then re-running what you wanted. Never wrap it in a timeout waiting for a human \u2014 `--wait`\n exists if you really want one blocking command, and killing that one is safe too (the request\n survives and the next command claims it). A command run before Confirm exits 1 naming the page and\n code again; once the 15 minutes are up it says to ask again. `lotics auth signup <email>` for an email that\n already has an account creates nothing and names `auth login`.\n- **A credential is either a SIGN-IN or an API KEY, and `logout` treats them differently.** A profile\n from `auth login` / `auth signup` acts as the person who confirmed it and is theirs \u2014 `lotics auth\n logout` revokes it server-side, and it lapses on its own after 90 idle days (each use pushes that\n out). A profile from `auth api-key` holds a key an ADMIN issued. A key created in Settings carries\n its OWN access \u2014 every app and table, or only the ones chosen on the key, so a listing that comes\n back short is the key's reach, not a bug \u2014 while a key created FOR a person carries that\n person's access and dies with their membership. Either is routinely also on a server and on\n other machines, so logout only forgets it locally and says so; only an admin revokes it. A profile that states no kind (saved before the field existed) is resolved against the SERVER\n and revoked only if the answer is a sign-in; a bare `--api-key` / `LOTICS_API_KEY` names no\n profile to remove at all. Nothing is ever revoked on a guess \u2014 between two, the destructive one is\n wrong. `lotics auth whoami` prints the kind, asking the server when the store cannot say.\n- **A key created in Settings never administers the organization, whatever its access.** The verbs\n `docs/cli_reference.md` marks *admin only* split in two under a key: `workspace doctor`, which only\n reads inside a workspace the key reaches, runs as before, while managing people, sharing or\n ownership, creating or deleting a workspace, changing workspace settings, setting credit limits,\n reading the access log and publishing an app's API answer `403` and name the remedy: an admin signed in, so\n `lotics auth login <email>` and run it again. A sign-in acts as that person and is refused none of\n them. Do not retry a `403` with the same credential and do not ask for a wider key \u2014 no answer on\n the key's own screen grants this.\n- **A 401 names its remedy \u2014 act on the hint, do not retry.** \"This credential expired / was\n revoked / belongs to a member who is no longer active\" carries the one remedy that ends this\n credential: run `lotics auth login <email>` for a sign-in, or ask the admin who issued it for a\n new key. A credential minted before that was recorded carries BOTH, because nothing on the row\n tells them apart \u2014 so on a box with no browser, take the second. Only an UNRECOGNIZED key gets\n the generic \"Invalid or disabled API key\", and that one is generic on purpose, so re-sending it\n teaches nothing. A `reason` rides on the body for a script to branch on, since the code stays\n `unauthorized` for every 401.\n- **Large payloads bypass `ARG_MAX`** \u2014 `lotics run <tool> @args.json`, or piped stdin behind `-`. A leading `@` is\n unambiguously a file path (JSON args start with `{`).\n- **stdout is the payload, stderr is the narration.** Progress, status lines, and the target echo go to\n stderr; the result goes to stdout, so piping stays clean. `--json` switches stdout from the\n agent-readable summary to the full structured object.\n- **Every tool is invoked one way \u2014 `lotics run <tool>`.** Including the ones that RUN something\n (`run_app_workflow`, `run_app_query`) and every one that builds\n (`set_tables`, `set_app`, `set_app_queries`, `set_app_workflow`). A command\n exists only for work that touches a local file.\n- **Building is two tools, each a writer that creates without an id and edits with one.** `set_tables`\n takes `{ \"tables\": [...] }` \u2014 always a list, one or many, all or nothing \u2014 each table a document keyed\n by alias (fields keyed by alias; a link names its table by id, or by the `ref` of another table of the\n same call, and states `multi`; a two-way link states `pair` on both tables), edited by an RFC 7396\n merge patch of that document, where `null` deletes. A call carries at most 50 tables, 200 first rows a table,\n and 2000 rows and 2000 documents in all. `set_app` takes ONE app: `{ \"body\": <the App tree> }` to create, the app's\n `name`, `description`, `icon` and `color` being props of the App node; `{ \"app_id\": \u2026, \"nodes\": { \"<address>\": <patch> } }`\n to change a node, refused with `base_version_id` when the app has moved past the version you read. `dry_run` checks\n without writing, a refusal lists everything wrong in the call, and every answer states the permissions the write\n granted and removed. The answer notes the treatments an author leaves unstated; nothing is written into the tree for them. A new workspace: one `set_tables` call with\n the tables, then every `set_app` call, then fixes. The example files under `https://lotics.ai/presets/index.json` are read and their tables passed to\n `set_tables`.\n- **Every change to an app mints a version of it, and applying the earlier tree again is the undo.** An apply, a\n deploy, a single `set_app_*` or `remove_app_binding`: each is a new version. Applying the earlier tree again restores the app \u2014 never a table change or a row a workflow wrote,\n so try a write on a throwaway record.\n- **Exit codes are assertable, and they report the WORK rather than the call.** `lotics run` exits\n non-zero when a `run_app_workflow` result's own envelope carries a failed\n `status` (`error`/`failed`/`cancelled`, or `not_previewed`: a dry run asked for and not given) \u2014 any other tool's top-level status is data and exits 0 \u2014\n so `lotics run \u2026 && next-step` cannot walk past a refused run; `workspace doctor` exits non-zero on\n findings. An unrecognized status exits 0 \u2014 the list is an allowlist of failure, so a status added\n later never turns a working script red \u2014 and a parked run (`awaiting_input`) is not a failure.\n- **An app that PUBLISHES an API turns every later binding write into a release.** Publishing\n snapshots what the app's queries, workflows and agents promise to callers outside it \u2014 a\n customer's own site or server, which nobody here can redeploy. From then on an additive change\n re-snapshots silently and a breaking one is REFUSED, naming each change; the tool's\n `acknowledge_breaking_api_change` carries it out and snapshots the break as a new contract version.\n- **Exposure is per app, all or nothing** \u2014 a public share or a key reaches every alias an app\n declares, so what outsiders may call is a second app over the same tables\n ([docs/building_an_app.md](./docs/building_an_app.md) \xA7 7).\n- **`--print-created` / `--cleanup` on any call that reports `side_effects`.** The first prints the\n records created plus a paste-ready cleanup plan and what cannot be auto-undone; the second runs\n those deletes (records only \u2014 never files, integrations or notifications). Neither is a rollback.\n- **Text output is the default and is built for reading**; reach for `--json` only when a field is\n needed programmatically.\n- **`lotics report '<json>'` is the channel for what nothing else records.** Reach for it\n when the platform is genuinely missing something (a capability that does not exist \u2014 no\n command ran), when a success was wrong (exited 0, wrong effect), or when an error did\n not name the remedy \u2014 not for your own mistakes, which the logs already show.\n **It is a frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, because a log\n reconstructs what you RAN and never what you WANTED, and that gap is the report. `goal` and\n `actual` are required; there is no severity or category to pick. The\n session's commands attach themselves \u2014 do not retype them. Run it bare for the full prompt; a\n long one rides `@file.json` or an explicit `-` for stdin (bare NEVER reads stdin).\n- **`LOTICS_TELEMETRY=1` correlates a whole session** so the authoring loop's rough edges can be\n found and fixed. Off by default; set it in the shell profile, not per command (each invocation is\n its own process). It sends no arguments, no file contents, and no record data \u2014 see README\n \xA7 Diagnostics.\n\n## Where this CLI is not the answer\n\n- **Editing an app in a local directory.** An app stated as a tree is changed by applying the\n tree again; its bindings through their `set_app_*` tools; a custom-code app's code by deploying\n its directory again. Nothing reads a local copy of a live app back.\n- **OAuth connections.** Attaching a connected account is web-only; the CLI can list them.\n";
1775
1775
 
1776
1776
  // docs/building_an_app.md
1777
- var building_an_app_default = '# Building an app, end to end\n\nThe other references here describe **grammars** \u2014 what a table document or an app tree may state, what a tool takes. This\none describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then\nreach for the area doc (`lotics docs`) whenever you need the detail.\n\n**The live app is the only edit surface.** Every change to an app \u2014 applying a tree, deploying a\nbuild, setting one query or workflow \u2014 mints a new version of it. To go back, apply the earlier tree\nagain (or deploy the earlier source). Nothing about an app lives in a local directory the platform\nreads back.\n\n**Going back restores the app, not the data.** A table change, and every row a workflow wrote while\nyou tried it, stay where they are. Try a write on a throwaway record.\n\n---\n\n## 1 \u2014 Two kinds of app\n\n- **An app stated as a tree** \u2014 the default, and the right one for almost every job. The tree (a\n JSON file, one per app) is a List of one table, whose rows are recognised by the\n 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\n reaches them, its Acts and the Checks that guard them (`lotics docs model/app-tree`,\n `model/list-node`, `model/section-node`, `model/act-node`, `model/check-node`). The platform\n compiles each app and draws it with the runtime every such app shares, so how each piece looks is\n the platform\'s, one way per concept, and no prop changes it. Every write the app makes is a\n generated workflow that re-checks on the server what the tables state.\n- **A custom-code app** \u2014 React you write, for a surface the tree\'s components cannot state. It\n reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever\n React you choose.\n\n## 2 \u2014 Clarify what is being asked, before designing it\n\n**A metric name is not a definition.** "Revenue", "in stock", "active", "overdue" \u2014 each is a\nbusiness rule the person asking owns, and the cost of guessing is a screen that is confidently\nwrong. Ask until there is no ambiguity left:\n\n- Which rows count, keyed off which field \u2014 a date, a status, a flag?\n- Does the same metric need a **different rule per table**? One table may key off a date and\n another off a status; one rule rarely covers both.\n- Snapshot or flow? "Current stock" is as-of-now; "revenue this month" is a window. They compile\n to different filters.\n- If the data cannot support the definition asked for \u2014 the field simply is not there \u2014 **say so\n and show the options.** Silently substituting a near-miss produces a number nobody can trace.\n\nThis is the step that gets skipped under time pressure, and it is the only one whose mistakes are\ninvisible in review: every later artifact is correct with respect to the wrong definition.\n\n## 3 \u2014 The data model, before any app\n\nGet this wrong and nothing above it can be precise. Each kind of thing is its own table with links into\nthe spine; attributes and evidence are fields on their owner. A single table with a `type` column\nstanding in for three kinds of thing collapses the distinctions every later query needs.\n\n**Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and\nlook at fill rates \u2014 a field that is present and empty on 90% of rows will not support the screen\nyou are about to design.\n\n**That includes imagery.** If the thing has a likeness \u2014 a product, a property, a vehicle, a\nperson \u2014 its picture is the strongest identifier a register row can carry, and an empty image field\nis a data gap to fill before you design around it: `lotics file upload`, then `update_records`.\n\n**Match on ids and option aliases, never on rendered text.** Resolve a name to its `rec_\u2026` or its option alias\nonce, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.\n\n**How the tables RELATE, and one fact in one column, is `lotics docs data_model`** \u2014 read it before designing a\nschema; those decisions outlive any one app.\n\n## 4 \u2014 An app stated as a tree\n\nTables first, then apps. `set_tables` takes a list of table documents, one or many, and writes them all or\nnone; `set_app` takes one app per call. Each creates when it is given no id and edits when it is given one.\n\n```\nlotics docs model # the keys of a table document and the app tree\'s components, with a worked example\nlotics docs design # the treatment per kind of row\nlotics run set_tables @tables.json # one call, every table: {"tables": [...]}\nlotics run set_app @orders.call.json # one app: {"body": <the App tree>} to create (add "dry_run": true to check)\nlotics app apply orders.app.json --check # the same tool from a bare tree file: what would refuse it, writing nothing\nlotics app apply orders.app.json # no app known: creates it; known: edits it to the file\nlotics app pull <app_id> # the app\'s tree, as ./<name>.app.json\n```\n\nA new workspace is built in one order: one `set_tables` call with the tables, then every `set_app`\ncall, then fixes. One call carries at most 50 tables, 200 first rows a table, and 2000 rows and 2000\ndocuments in all; a workspace of more tables is built in further calls, each naming the tables already made by\ntheir ids. The example files at `https://lotics.ai/presets/index.json` are read and their tables passed to\n`set_tables`.\n\nA table is a document: its `fields` are keyed by alias, and a link field names the table it\npoints at by id, or by the `ref` of another table of the same call, and states `multi`. A\ntwo-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:\nwhat the patch leaves alone is left alone and `null` deletes. `dry_run` checks the call and writes\nnothing; a refusal lists everything wrong in it at once.\n\nThe App node is the app: its `name`, `description`, `icon` and `color` are props of it, and no other input of\n`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);\n`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\nis `{"body": <the App tree>}`; one that changes a part is `{"app_id": "app_\u2026", "nodes": {"<address>": <patch>}}`,\neach address a node\'s path of keys (`get_app` prints every one), a patch a JSON merge patch of that node\'s props\nand `null` removing the node; the App\'s own props change through `body`. A call stating `base_version_id` \u2014 the\nversion you read \u2014 is refused when the app has moved on. The old body grammar (`register`, `record`, `acts`,\n`edits`) is refused with a sentence pointing here.\n\nA tree states what it means once: a statement equal to what the runtime draws anyway (a column the row already shows) is kept,\ndrawn 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.\n`type` may be omitted, and a Section, Side or Page\nwithout a `key` is keyed by its title\'s slug, written once and kept.\n\n`app apply` with no app known creates one from the file, named by the tree\'s `name`, and records its id and\nversion in `<file>.lotics.json` (`app pull` writes the same file), so the next apply of that file edits that\napp; `--app <app_id>` names it instead, and is refused for a file whose record names another app. A known app\nis read first and only what the file changes is sent, as a JSON merge patch (RFC 7396): a key the file drops\nis removed, a list is replaced whole, and what the file leaves alone is left alone. A file pulled at a\nversion the app has since left is refused, naming the version: pull the current tree into another path and\ncarry your edits over. Applying a tree the app already has mints nothing.\n`--check` sends the same change as `dry_run`: what would refuse the tree, the notes on it, and the permissions the\nwrite would grant and remove, writing nothing. Every write\'s answer states those permissions: the saves, adds,\nDeletes and act writes the new tree grants and removes, from the previous tree to the new one.\n\n**Nothing is written into the tree for you.** The answer notes a treatment the rows call for that the tree leaves out \u2014\nReadings over its rows, a record\'s Sections, an Act per status move, a picture where its rows hold photos\n(`lotics docs design`) \u2014 and the tree states the ones the job needs.\n\n**The tree changes after the app exists, and applying it again is how it lands.** Pull the tree, edit\nthe 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\nthe live one, and `lotics run set_app_workflow` changes it. A job no press starts \u2014 an agent or a\nservice handing the app a file or a list of rows \u2014 is a Workflow node the app declares\n(`lotics docs model/workflow-node`): the node states what it takes and hands back, and `lotics run\nset_app_workflow` sets its code with `inputs` and `outputs` left out.\n\n**What a person reads and does in the app, an agent reads and does through it.** Writing the tree generates a\nquery for every List, record, child List and figure the screen reads, and a workflow for every add, edit,\nremoval and act \u2014 `lotics run get_app` lists them, and `lotics docs agent_surface` names each\nfamily and how it is called. A question the screen never asks is a Query node the app declares\n(`lotics docs model/query-node`), beside its Workflow nodes. Both are part of the design: `lotics docs\nagent_surface`, \xA7 Declaring more.\n\n## 5 \u2014 A custom-code app\n\n```\nlotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk\ncd <dir>\n# edit src/App.tsx \u2014 node_modules/@lotics/app-sdk/AGENTS.md is the reference\nnpm run typecheck && npm run lint && npm test\nlotics app deploy -m "<what changed>" # build, upload, a new version live\nlotics app pull <app_id> # the live version\'s source, on a machine without the project or after another deploy\n```\n\nWhat the app reads and writes is bound on the app, never in the project: `lotics run\nset_app_queries` binds named queries, `lotics run set_app_workflow` a workflow body, and each mints a\nversion. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build\nand carries every binding forward unchanged.\n\n**Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column\n(`{ "source": "amount", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with\n`is_current_member` **inside the query** \u2014 a member id passed from the client is chosen by the\ncaller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.\n\n**Workflows are the only way an app writes**, and every query and workflow bound to an app is also what\nan agent reads and runs there: `lotics docs agent_surface` \xA7 Declaring more states how each is shaped for\nthat caller.\n\n## 6 \u2014 Prove it, without a screen\n\nStatic checks prove parse, types and names \u2014 they evaluate nothing. Rehearse a write first:\n\n```\nlotics run set_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","source":"\u2026","verify_only":true,"record_id":"rec_\u2026"}\'\n```\n\nIt walks the draft through the same engine an Act\'s preview uses, under the app\'s authority: reads are real,\nwrites and sends are said and not made, and it returns what the run would answer plus the calls it would make.\nPass `preview_inputs` for the values of a workflow that takes inputs and no row.\n\nThen run it end to end, on a throwaway record:\n\n```\nlotics run run_app_query \'{"app_id":"app_\u2026","alias":"\u2026","params":{\u2026}}\'\nlotics run run_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","inputs":{\u2026}}\'\n# exits non-zero when the run failed, so it is assertable\n```\n\nA workflow is also how an app **produces a document** \u2014 `generate_document` fills a template\nyou registered once (`lotics docs document_templates`).\n\n## 7 \u2014 Look at it, then name it\n\nLook at every app you apply, at a desktop width and at a phone\'s \u2014 every check above reads the\ndefinition, none of them the pixels:\n\n```\nlotics run screenshot_app \'{"app_id":"app_\u2026"}\' -o ./shots\n# one PNG per width saved into ./shots, each with its text snapshot beside it as <name>.txt;\n# "path":"/rec_\u2026" opens a screen inside the app\n```\n\nFrom the CLI each image is a link signed for an hour, and `-o` saves it. Each shot is one screen, as a member sees it; `"screen":2` draws the next screen of\nthe content scrolling inside the app, and so on to its end, and `"full_page":true` grows one image\nto the first five screens \u2014 one or the other. Beside each image comes its `snapshot`, the screen\'s\naccessibility tree as text \u2014 read it to check labels and values without opening the picture \u2014 and,\nwhere a table left columns off at that width, `shed` names them.\n\nThe app is drawn live, as you, read-only: a screen that writes when it opens shows that write\nrefused, and `errors` lists what failed on the page. A version that looks wrong is one\n`lotics app apply` away from the one before: apply the earlier tree again.\n\nFor an app stated as a tree, `verdict` comes beside the images: what a check of that app finds now,\nbeside its rows \u2014 what the next write would refuse it for and the notes\non it. `verdict_unread` says why none was read.\n\nThe app\'s `name`, `icon`, `color` and `description` are props of the App node; an app\nstating no `color` gets the platform\'s grey tile. `lotics run set_app` with the app\'s id and a `body` patch changes them\non the app, and `lotics app pull` reads the tree back as the app holds it. The\n`description` heads the capability listing the member\'s chat agent reads on **every** turn, so a\nstanding process the app expects that agent to carry out belongs there and nowhere else.\n\n**A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key\naccess to it, reaches every alias the app declares; no alias can be held back. So whatever\noutsiders may call is a SECOND app over the same tables: only the queries they may read and the\nworkflows they may run. The desk the team works in stays a separate, private app.\n';
1777
+ var building_an_app_default = '# Building an app, end to end\n\nThe other references here describe **grammars** \u2014 what a table document or an app tree may state, what a tool takes. This\none describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then\nreach for the area doc (`lotics docs`) whenever you need the detail.\n\n**The live app is the only edit surface.** Every change to an app \u2014 applying a tree, deploying a\nbuild, setting one query or workflow \u2014 mints a new version of it. To go back, apply the earlier tree\nagain (or deploy the earlier source). Nothing about an app lives in a local directory the platform\nreads back.\n\n**Going back restores the app, not the data.** A table change, and every row a workflow wrote while\nyou tried it, stay where they are. Try a write on a throwaway record.\n\n---\n\n## 1 \u2014 Two kinds of app\n\n- **An app stated as a tree** \u2014 the default, and the right one for almost every job. The tree (a\n JSON file, one per app) is a List of one table, whose rows are recognised by the\n 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\n reaches them, its Acts and the Checks that guard them (`lotics docs reference/app-tree`,\n `reference/list-node`, `reference/section-node`, `reference/act-node`, `reference/check-node`). The platform\n compiles each app and draws it with the runtime every such app shares, so how each piece looks is\n the platform\'s, one way per concept, and no prop changes it. Every write the app makes is a\n generated workflow that re-checks on the server what the tables state.\n- **A custom-code app** \u2014 React you write, for a surface the tree\'s components cannot state. It\n reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever\n React you choose.\n\n## 2 \u2014 Clarify what is being asked, before designing it\n\n**A metric name is not a definition.** "Revenue", "in stock", "active", "overdue" \u2014 each is a\nbusiness rule the person asking owns, and the cost of guessing is a screen that is confidently\nwrong. Ask until there is no ambiguity left:\n\n- Which rows count, keyed off which field \u2014 a date, a status, a flag?\n- Does the same metric need a **different rule per table**? One table may key off a date and\n another off a status; one rule rarely covers both.\n- Snapshot or flow? "Current stock" is as-of-now; "revenue this month" is a window. They compile\n to different filters.\n- If the data cannot support the definition asked for \u2014 the field simply is not there \u2014 **say so\n and show the options.** Silently substituting a near-miss produces a number nobody can trace.\n\nThis is the step that gets skipped under time pressure, and it is the only one whose mistakes are\ninvisible in review: every later artifact is correct with respect to the wrong definition.\n\n## 3 \u2014 The data model, before any app\n\nGet this wrong and nothing above it can be precise. Each kind of thing is its own table with links into\nthe spine; attributes and evidence are fields on their owner. A single table with a `type` column\nstanding in for three kinds of thing collapses the distinctions every later query needs.\n\n**Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and\nlook at fill rates \u2014 a field that is present and empty on 90% of rows will not support the screen\nyou are about to design.\n\n**That includes imagery.** If the thing has a likeness \u2014 a product, a property, a vehicle, a\nperson \u2014 its picture is the strongest identifier a register row can carry, and an empty image field\nis a data gap to fill before you design around it: `lotics file upload`, then `update_records`.\n\n**Match on ids and option aliases, never on rendered text.** Resolve a name to its `rec_\u2026` or its option alias\nonce, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.\n\n**How the tables RELATE, and one fact in one column, is `lotics docs data_model`** \u2014 read it before designing a\nschema; those decisions outlive any one app.\n\n## 4 \u2014 An app stated as a tree\n\nTables first, then apps. `set_tables` takes a list of table documents, one or many, and writes them all or\nnone; `set_app` takes one app per call. Each creates when it is given no id and edits when it is given one.\n\n```\nlotics docs reference # the keys of a table document and the app tree\'s components, with a worked example\nlotics docs design # the treatment per kind of row\nlotics run set_tables @tables.json # one call, every table: {"tables": [...]}\nlotics run set_app @orders.call.json # one app: {"body": <the App tree>} to create (add "dry_run": true to check)\nlotics app apply orders.app.json --check # the same tool from a bare tree file: what would refuse it, writing nothing\nlotics app apply orders.app.json # no app known: creates it; known: edits it to the file\nlotics app pull <app_id> # the app\'s tree, as ./<name>.app.json\n```\n\nA new workspace is built in one order: one `set_tables` call with the tables, then every `set_app`\ncall, then fixes. One call carries at most 50 tables, 200 first rows a table, and 2000 rows and 2000\ndocuments in all; a workspace of more tables is built in further calls, each naming the tables already made by\ntheir ids. The example files at `https://lotics.ai/presets/index.json` are read and their tables passed to\n`set_tables`.\n\nA table is a document: its `fields` are keyed by alias, and a link field names the table it\npoints at by id, or by the `ref` of another table of the same call, and states `multi`. A\ntwo-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:\nwhat the patch leaves alone is left alone and `null` deletes. `dry_run` checks the call and writes\nnothing; a refusal lists everything wrong in it at once.\n\nThe App node is the app: its `name`, `description`, `icon` and `color` are props of it, and no other input of\n`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);\n`set_app` fills the `tables` and `templates` maps by id from them and `get_app` shows aliases (`lotics docs reference/app-node`). A call that creates\nis `{"body": <the App tree>}`; one that changes a part is `{"app_id": "app_\u2026", "nodes": {"<address>": <patch>}}`,\neach address a node\'s path of keys (`get_app` prints every one), a patch a JSON merge patch of that node\'s props\nand `null` removing the node; the App\'s own props change through `body`. A call stating `base_version_id` \u2014 the\nversion you read \u2014 is refused when the app has moved on. The old body grammar (`register`, `record`, `acts`,\n`edits`) is refused with a sentence pointing here.\n\nA tree states what it means once: a statement equal to what the runtime draws anyway (a column the row already shows) is kept,\ndrawn 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.\n`type` may be omitted, and a Section, Side or Page\nwithout a `key` is keyed by its title\'s slug, written once and kept.\n\n`app apply` with no app known creates one from the file, named by the tree\'s `name`, and records its id and\nversion in `<file>.lotics.json` (`app pull` writes the same file), so the next apply of that file edits that\napp; `--app <app_id>` names it instead, and is refused for a file whose record names another app. A known app\nis read first and only what the file changes is sent, as a JSON merge patch (RFC 7396): a key the file drops\nis removed, a list is replaced whole, and what the file leaves alone is left alone. A file pulled at a\nversion the app has since left is refused, naming the version: pull the current tree into another path and\ncarry your edits over. Applying a tree the app already has mints nothing.\n`--check` sends the same change as `dry_run`: what would refuse the tree, the notes on it, and the permissions the\nwrite would grant and remove, writing nothing. Every write\'s answer states those permissions: the saves, adds,\nDeletes and act writes the new tree grants and removes, from the previous tree to the new one.\n\n**Nothing is written into the tree for you.** The answer notes a treatment the rows call for that the tree leaves out \u2014\nReadings over its rows, a record\'s Sections, an Act per status move, a picture where its rows hold photos\n(`lotics docs design`) \u2014 and the tree states the ones the job needs.\n\n**The tree changes after the app exists, and applying it again is how it lands.** Pull the tree, edit\nthe 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\nthe live one, and `lotics run set_app_workflow` changes it. A job no press starts \u2014 an agent or a\nservice handing the app a file or a list of rows \u2014 is a Workflow node the app declares\n(`lotics docs reference/workflow-node`): the node states what it takes and hands back, and `lotics run\nset_app_workflow` sets its code with `inputs` and `outputs` left out.\n\n**What a person reads and does in the app, an agent reads and does through it.** Writing the tree generates a\nquery for every List, record, child List and figure the screen reads, and a workflow for every add, edit,\nremoval and act \u2014 `lotics run get_app` lists them, and `lotics docs agent_surface` names each\nfamily and how it is called. A question the screen never asks is a Query node the app declares\n(`lotics docs reference/query-node`), beside its Workflow nodes. Both are part of the design: `lotics docs\nagent_surface`, \xA7 Declaring more.\n\n## 5 \u2014 A custom-code app\n\n```\nlotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk\ncd <dir>\n# edit src/App.tsx \u2014 node_modules/@lotics/app-sdk/AGENTS.md is the reference\nnpm run typecheck && npm run lint && npm test\nlotics app deploy -m "<what changed>" # build, upload, a new version live\nlotics app pull <app_id> # the live version\'s source, on a machine without the project or after another deploy\n```\n\nWhat the app reads and writes is bound on the app, never in the project: `lotics run\nset_app_queries` binds named queries, `lotics run set_app_workflow` a workflow body, and each mints a\nversion. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build\nand carries every binding forward unchanged.\n\n**Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column\n(`{ "source": "amount", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with\n`is_current_member` **inside the query** \u2014 a member id passed from the client is chosen by the\ncaller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.\n\n**Workflows are the only way an app writes**, and every query and workflow bound to an app is also what\nan agent reads and runs there: `lotics docs agent_surface` \xA7 Declaring more states how each is shaped for\nthat caller.\n\n## 6 \u2014 Prove it, without a screen\n\nStatic checks prove parse, types and names \u2014 they evaluate nothing. Rehearse a write first:\n\n```\nlotics run set_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","source":"\u2026","verify_only":true,"record_id":"rec_\u2026"}\'\n```\n\nIt walks the draft through the same engine an Act\'s preview uses, under the app\'s authority: reads are real,\nwrites and sends are said and not made, and it returns what the run would answer plus the calls it would make.\nPass `preview_inputs` for the values of a workflow that takes inputs and no row.\n\nThen run it end to end, on a throwaway record:\n\n```\nlotics run run_app_query \'{"app_id":"app_\u2026","alias":"\u2026","params":{\u2026}}\'\nlotics run run_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","inputs":{\u2026}}\'\n# exits non-zero when the run failed, so it is assertable\n```\n\nA workflow is also how an app **produces a document** \u2014 `generate_document` fills a template\nyou registered once (`lotics docs document_templates`).\n\n## 7 \u2014 Look at it, then name it\n\nLook at every app you apply, at a desktop width and at a phone\'s \u2014 every check above reads the\ndefinition, none of them the pixels:\n\n```\nlotics run screenshot_app \'{"app_id":"app_\u2026"}\' -o ./shots\n# one PNG per width saved into ./shots, each with its text snapshot beside it as <name>.txt;\n# "path":"/rec_\u2026" opens a screen inside the app\n```\n\nFrom the CLI each image is a link signed for an hour, and `-o` saves it. Each shot is one screen, as a member sees it; `"screen":2` draws the next screen of\nthe content scrolling inside the app, and so on to its end, and `"full_page":true` grows one image\nto the first five screens \u2014 one or the other. Beside each image comes its `snapshot`, the screen\'s\naccessibility tree as text \u2014 read it to check labels and values without opening the picture \u2014 and,\nwhere a table left columns off at that width, `shed` names them.\n\nThe app is drawn live, as you, read-only: a screen that writes when it opens shows that write\nrefused, and `errors` lists what failed on the page. A version that looks wrong is one\n`lotics app apply` away from the one before: apply the earlier tree again.\n\nFor an app stated as a tree, `verdict` comes beside the images: what a check of that app finds now,\nbeside its rows \u2014 what the next write would refuse it for and the notes\non it. `verdict_unread` says why none was read.\n\nThe app\'s `name`, `icon`, `color` and `description` are props of the App node; an app\nstating no `color` gets the platform\'s grey tile. `lotics run set_app` with the app\'s id and a `body` patch changes them\non the app, and `lotics app pull` reads the tree back as the app holds it. The\n`description` heads the capability listing the member\'s chat agent reads on **every** turn, so a\nstanding process the app expects that agent to carry out belongs there and nowhere else.\n\n**A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key\naccess to it, reaches every alias the app declares; no alias can be held back. So whatever\noutsiders may call is a SECOND app over the same tables: only the queries they may read and the\nworkflows they may run. The desk the team works in stays a separate, private app.\n';
1778
1778
 
1779
1779
  // docs/cli_reference.md
1780
- var cli_reference_default = "# @lotics/cli \u2014 CLI Command Reference\n\nPer-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. Start at [AGENTS.md](../AGENTS.md) for the model this reference assumes; `lotics --help` is the authoritative, always-current verb list.\n\n| Command | What it does |\n|---|---|\n| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (\xA7 COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics app --help`, `lotics file download --help`); `lotics report --help` prints the report frame. |\n| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open \u2014 `https://lotics.ai/cli_login/<request_id>`, also mailed \u2014 and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential \u2014 `docs` among them \u2014 claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |\n| \u2014 | **A flag is refused on a verb that does not read it**, naming the verbs that do, before any request or credential: `--check` on `lotics run` would otherwise be parsed and ignored, and the write it was meant to stop would run. `--json`, `--workspace`, `--api-key` and `--timeout` are read by every verb. |\n| `lotics auth api-key [key]` | `whoami` \u2192 **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth web` | Send a magic link email to access the web app (requires auth) |\n| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key \u2014 read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |\n| `lotics auth logout [<name\\|id>]` | In a pinned dir: delete the local pin. Else: remove the profile (default the active org), `--all` for every one. What happens server-side depends on which KIND of credential it is. A **sign-in** (`auth login` / `auth signup`) is revoked \u2014 logging that terminal out ends its credential rather than leaving a live one behind; a server that cannot be reached, or a credential already dead, never blocks the local forget, and one line names the org and Settings \u2192 Security \u2192 *Keys and terminals*. An **API key** (`auth api-key`) is only forgotten here \u2014 an admin issued it and it is routinely on a server and on other machines, so one terminal signing out must not kill it for everyone; the line says it is still active and names both pages, because Settings \u2192 API keys is admin-only and the credential may well be the holder's own sign-in, which they revoke themselves at Settings \u2192 Security \u2192 *Keys and terminals*. A profile saved before the kind was recorded states nothing, so the SERVER is asked (`auth whoami`) and it is revoked only if the answer is a sign-in: an older server, a credential minted before the column, and a request that fails all leave it alone. |\n| \u2014 | **A refused credential says which of three ways it is dead, and names the remedy that ends its kind.** `This credential expired.` / `was revoked.` / `belongs to a member who is no longer active in this organization.` carries `Run \\`lotics auth login <email>\\` to sign in again.` for a sign-in and `Ask an admin for a new API key (Settings \u2192 API keys).` for an issued key. A credential minted before that was recorded still gets BOTH in one sentence, because nothing on the row tells them apart \u2014 so a headless box is never sent looking for a browser alone. A key the server does not recognize at all gets one flat `Invalid or disabled API key.` \u2014 deliberately, so a guessed key learns nothing, not even that it named a row. The body carries `reason` for a script to branch on, since the code stays `unauthorized` for every 401. |\n| `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |\n| `LOTICS_ORG=<name\\|id>` | Scope every command in this shell to one saved org. **Resolved once, before any command dispatches**, so a value matching no saved credential refuses every verb with one sentence \u2014 a read, a write, and a local check that needs no credential alike \u2014 and refuses it before the first byte is written. It refuses even when a credential arrives another way, because `--api-key` / `LOTICS_API_KEY` outrank it in the precedence chain and a write must never fall through to whatever THOSE name while the variable says otherwise; when the variable resolves and a key is also given, the key decides and the command says so. The refusal lists the orgs this machine holds, so it is answerable without another command (`lotics org` is refused by the same rule). A name is whatever the credential was SAVED under \u2014 a server-side rename never moves it, and the new name resolves too, so both keep working and `lotics org` prints the pair. |\n| `lotics org use <name\\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous \u2192 error) or id. No flag \u2192 global `active_org`; `--local` \u2192 a `.lotics/config.json` pointer in the current dir. |\n| `lotics workspace` | List workspaces in the active org, marks current with `(current)` |\n| `lotics workspace select <id>` | Set the workspace in the **active scope** \u2014 a local pin if the dir has one, else the active org's global profile. Records the workspace's NAME beside its id, which is what the `lotics \u2192 <org> / <workspace>` echo prints; `workspace list`, `workspace create`, `workspace rename` and `org use` record it too, so a target is named rather than identified. Until one command has listed it, the echo prints the id and says the name is not known yet. |\n| `lotics workspace create <name> [--timezone <Area/City>] [--currency <ISO>]` | Create a new workspace (admin only), auto-switches to it. Neither flag is defaulted from THIS machine, unlike signup: an extra workspace is routinely created by an operator for somebody else. Without `--timezone` the new workspace inherits the zone of the org's OLDEST workspace; without `--currency` it takes the org's default. Both ride the create, so the workspace is never briefly denominated in a currency nobody asked for. `--currency` takes an ISO-4217 code (case-insensitive; anything else is refused). |\n| `lotics workspace rename <name>` | Rename the **current** workspace (admin only) \u2014 the endpoint takes its target from the request's workspace, never a path id, so switch with `workspace select <id>` first and read the `lotics \u2192 <org> / <workspace>` echo before trusting it \u2014 both halves are names, and the rename moves the cached one in the same act. Carries the workspace's existing `default_currency` and `timezone` through unchanged: the endpoint takes the whole settings triple, so sending only a name would blank the other two. |\n| `lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]` | Change the CURRENT workspace's name, default currency or timezone \u2014 `PATCH /v1/workspace`, admin only. Only what you name changes; the endpoint takes the whole triple, so the CLI carries the two you did not. `rename` is this verb with the name alone, which is why it can never forget the other two. Both values are invisible once they are wrong: the currency decides how every money field RENDERS and the zone decides how every date BUCKETS, on a workspace whose whole purpose may be to look like the customer's own. `--json` prints the updated workspace. |\n| `lotics workspace delete <id> --yes` | Delete a workspace by id (admin only). **Soft delete** \u2014 `archived_at` is set, so it drops out of listings, can no longer be selected, and its tables/records go dark, while the data is retained and recoverable. Its **apps are cascade-archived** too \u2014 every app entry point (embedded, public link, standalone subdomain, incl. anonymous public links) stops serving. Refuses the org's **only** active workspace (400) and any workspace outside the caller's org (404). Requires `--yes` to confirm (destructive; the CLI is used non-interactively). |\n| `lotics workspace doctor` | Report workspace-wide dangling schema references via `GET /v1/workspaces/dangling-references` \u2014 every active app/workflow artifact whose prefixed schema id no longer resolves, printed as `<referent.kind> \"<name>\" (<id>) \u2192 <namespace> <id> (missing)`; healthy prints a one-line all-clear. **Exits non-zero (exit 1) on findings** so scripts can gate on it. Resolves the first workspace like every data command (runs before the global workspace resolution). Admin-only. |\n| `lotics tools` | List tools by category with descriptions |\n| `lotics tools <name>` | Full description, input JSON Schema, and the output schema where the tool answers a shape, for one tool; a workflow step says to call it from a workflow body, not `lotics run` |\n| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \\| lotics run <tool> -`) \u2014 both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone \u2014 the error names both escapes. |\n| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |\n| `lotics run <tool>` \u2014 **file cells** | A file in a tool's result carries its `fil_\u2026` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file \u2014 this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |\n| \u2014 | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_query`) and every one that builds: `set_tables` (a list of table documents), `set_app` (one app), `set_app_queries`, `set_app_workflow`. A command exists only for work that touches a local file: `app apply`, `app create --custom`, `app pull`, `app deploy`. |\n| \u2014 | **An app's queries and workflows take more than their params and inputs** (`lotics docs agent_surface`). `run_app_query '{\"app_id\",\"alias\",\"params\"}'` also takes `filters` and `sort` over the query's output columns, `fields` (the columns to return), `count` with `count_by` or `count_where`, `aggregate`, `limit` (200 rows at most; 50 where none is stated) and `cursor`, and `runs` \u2014 up to 20 reads in one call, each `{\"alias\", \u2026}`, a refused one answered with its `error`; one call's rows total at most 262,144 bytes. `export: {\"key\", \u2026narrowing}` instead of the read arguments makes the file an app's Export makes (`lotics docs agent_surface`). `run_app_workflow '{\"app_id\",\"alias\",\"inputs\"}'` also takes `dry_run: true` \u2014 a rehearsal on the real inputs, nothing written or sent; the answer is what the run would return, with the reads it made under `dry_run.reads`, the calls it would make under `dry_run.writes` and the papers it rendered under `dry_run.papers` and the parts of the answer it could not say under `dry_run.unpredicted`; a workflow no dry run is offered of answers `status: \"not_previewed\"` \u2014 and `runs` \u2014 up to 50 runs in order, the first refused, failed or not previewed ending the call, an input `{\"from_run\": <index>, \"path\": \"data.<key>\"}` reading an earlier run's answer. |\n| \u2014 | **The exit code reports the WORK, not just the call \u2014 for the tool that RUNS one.** `run_app_workflow` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`, or `not_previewed` \u2014 a dry run asked for and not given) exit non-zero and print `<tool> \u2192 <status>: <message>` to stderr, so `lotics run \u2026 && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE \u2014 an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |\n| `lotics run <tool> '<json>' -o <dir>` | Also save every **signed link** the answer carries \u2014 an object with a `url`, a `filename` and the `expires_at` its signature ends at, at any depth, such as `screenshot_app`'s images. Each is fetched into `<dir>` (created if missing) under its `filename`, taking a free spelling beside a file of that name already there, and a link's `snapshot` text is written beside it as `<name>.txt`. The written paths follow the answer on **stdout**, one per line (on stderr under `--json`, whose stdout stays the one object). A link is signed for an hour; one the store no longer serves exits non-zero. A workspace file states no `expires_at` and is left to `lotics file download`. |\n| `lotics run set_member '{\"members\": {\"<email or member id>\": {\"role\": \"member\", \"name\": \"\u2026\"}}}'` | Invite, rename, remove (`null`) or restore members, admin only: the whole map is checked before an email is sent. A member's `lotics tools` does not list it, nor `set_group` or `set_workspace`. View-as is the web app's preview alone; the CLI acts as the signed-in member. |\n| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |\n| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes \u2014 harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |\n| `lotics file upload <file\\|dir...>` (alias `lotics upload`) \xB7 `--stdin` \xB7 `--base64` \xB7 `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files`, several such files to a request up to half that route's 100 MiB cap; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose \u2014 one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** \u2014 an attachment decoded in memory, a generated document, a signed download link \u2014 each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY \u2014 `Buffer.from(s, \"base64\")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |\n| `lotics file download <file_id> [<path>]` \xB7 `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` \u2192 fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there \u2014 the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once \u2014 a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_alias> [-o <dir>]` (the field named by the alias `get_record` shows it under) spreads every file on a record's file field over a DIRECTORY \u2014 there is no single file for N files to be. |\n| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first \u2014 id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset \u2014 pass it back as given \u2014 so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to \"what is in here\" short of reading Postgres. |\n| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. Deleting an original deletes every version of it; a file's current version cannot be deleted on its own, and a document template's file, or a version of it, only by a member who manages the template. **Refused while a record cell, a comment, a knowledge doc, a document template, a voice session or a pending change request still references the file or a version it takes along** \u2014 the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what \"deleted\" means here. There is no `lotics delete`: the verb needs its noun. |\n| `lotics knowledge list [--include-hidden \\| --deleted]` | `GET /v1/knowledge_docs` \u2014 a table of id, name, tags, description (`--json` for the docs). **REST, not `get_workspace` kind `knowledge`**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. **`--deleted` is the one tool-backed form**: it calls `get_workspace` with `kind: \"knowledge\"` and `include_archived: true`, page by page, and prints the deleted docs you can manage (id, name, deleted time, description); it cannot be combined with `--include-hidden`. |\n| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \\| --content <str>)` | Read the body client-side (a file XOR an inline string \u2014 exactly one required), then call `set_knowledge` with `{ name, description, content, tags? }` and no `knowledge_doc_id`, which makes a doc (description defaults to `\"\"`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |\n| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) \u2192 the doc with its **hydrated `content`** (the one whole-content read for a non-sandbox client; the tool `read_knowledge` reads a line range). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |\n| `lotics knowledge update <id> [--from <file.md> \\| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `set_knowledge` with a `knowledge_doc_id` and **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set \u2014 the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |\n| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` \u2014 one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |\n| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** \u2014 the Library's list, and the corpus `get_workspace` lists and its `search` searches \u2014 while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as \"everything\". |\n| `lotics knowledge rm <id...>` | Delete docs via `delete_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a live doc you manage that no app or workflow names, or nothing is deleted. Sharing and history are kept. The REST execute path does not gate `needsApproval`, so this runs unattended. |\n| `lotics knowledge restore <id...>` | Bring deleted docs back via `restore_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a deleted doc you manage, or nothing is restored. Refuses the no-argument form. |\n| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty \u2014 before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`GET /v1/apps/{id}/types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: a JSON app is made by `lotics app apply` (the tool `set_app`). The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |\n| `lotics app pull [app_id] [path]` | **An app's live definition, pulled.** The app is read with `get_app`: its author is shown a body, a JSON **tree** (with the addresses of its nodes) pulled as that tree and a custom-code app (the identity alone) as its source; an app whose stored tree cannot be read is refused with the reason `get_app` gives, and so is an app that shows the caller no body (the caller does not author it, or it has no version). **A JSON app**: the tree is written as JSON to `[path]` (a directory receives `./<name>.app.json`; default `./<name>.app.json`), and the app's id and current version beside it in `<file>.lotics.json` (`{ \"app_id\": \"\u2026\", \"version_id\": \"\u2026\", \"digest\": \"\u2026\" }`, the digest being the file as the CLI last left it; a record without all three is refused by `app apply`, naming `lotics app pull`) \u2014 a custom-code project records both in `package.json#lotics`, a single file has no such place \u2014 so the next `lotics app apply <file>` edits that app, and is refused if the app has since moved past that version. An existing file is overwritten only when its `.lotics.json` names this same app and the file is unchanged since it was last pulled or applied; a file with edits, or whose record names the app but no digest, is refused, naming `--force`, which overwrites it. `--force` is a flag of `app pull` alone. `--json` prints `{workspace_id, app_id, path, outcome}`. **A custom-code app**: its live source, as a project ready to deploy on it. The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** \u2014 but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`GET /v1/apps/{id}/types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). |\n| `lotics app apply <file> [--app <app_id>] [--check] [--json]` | **A JSON app, one at a time, through the tool `set_app`.** The file is an app's tree (`lotics docs model`: the App node with the nodes under it, its tables and templates named by alias). **No app known** (no `--app`, no `<file>.lotics.json` beside it): the file is sent as `{ \"body\": \u2026 }` to make a new app, named by the body's `name`, and the id made is printed, then recorded with its version in `<file>.lotics.json`. **An app known**: its current tree is read (`get_app`) and the **RFC 7396 merge patch** that turns it into the file is sent as `body` with the `app_id` and the version the patch was computed against as `base_version_id` \u2014 a key the file drops is `null` in the patch, a list is replaced whole, what the file leaves alone is left alone, and a tree the app already has sends nothing and says so. **A file pulled at a version the app has since left is refused** before anything is sent, naming both versions and `lotics app pull <app_id> <path>`: pull the current tree into another path, carry the edits over, apply that file. `--app` naming an app other than the sibling file's is refused too. A write that races the apply is refused by the server, which names the app's current version. A file that sets an object key to `null` is refused naming the key, since a merge patch reads `null` as a removal. `--app` names the app, else the sibling file does; an app whose `get_app` shows no body (it is not yours) or whose stored tree cannot be read is refused, and a custom-code app takes its identity (name, description, icon, colour) only, and no `<file>.lotics.json` records it, since an identity write makes no version \u2014 its source deploys with `app deploy`. **The file is never rewritten**: a node the tree leaves out draws as the rows call for it and is stored nowhere (`lotics docs design`). It prints what `set_app` answered and reports its `outcome`: its notes and findings, every finding at once, and the saves, adds and Deletes the write grants and removes. **`--check` sends `dry_run`** (a flag of `app apply` alone, as is `--app`) \u2014 what would refuse the tree, and what it would grant and remove \u2014 and writes nothing, the file and its sibling included; an `outcome` of `refused` exits 1. A tree the app already has, or one `set_app` answers `unchanged`, records the version the app stays at in the sibling. **To go back to an earlier version, apply the earlier tree again.** Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, app_id, outcome, write}`, `outcome` being `set_app`'s: `created`, `updated`, `unchanged` or `refused` (with `--check`, what it would be). |\n| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`GET /v1/apps/{id}/types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent; a type error in the app's own files stops the deploy, one inside an installed package is reported and does not) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged \u2014 those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |\n| `lotics docs` \\| `lotics docs <area>[/<section>]` \\| `lotics docs [<area>[/<section>]] --grep <text>` | **This CLI's own references, carried inside the binary** \u2014 the model reference, this index, and every doc under `docs/` \u2014 so the doc a reader opens describes the binary answering, listed by the job a reader comes to do. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. **A reference the binary's copy lacks, or a part of one the server serves, is read from the server's `docs` tool** with this machine's credential, after the copy's refusal \u2014 a page the server added since this binary was built, which a server refusal can cite. A name matching more than one reference, or a part missing from a guide to this CLI, is answered by the copy alone. **`--grep` searches the references the server serves** (not this CLI's own guides, which it refuses), with this machine's credential, narrowed to the area or section named: literal text unless `--regex`; a literal search that finds nothing while the pattern read as an expression matches (`a|b`) returns those lines under a note saying so, and `--literal` keeps the literal zero; with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` \u2014 the dialect the knowledge lines of `get_workspace` with `search` are read in \u2014 each hit under the address that opens its page; any of those options without `--grep` is refused. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |\n| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script \u2014 the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe \u2014 a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |\n| `lotics docs model` \\| `lotics docs model/<section>[/\u2026]` | **The reference for tables and apps, from inside the binary** \u2014 at this CLI's version; `lotics docs` lists it under \"Build an app\", after `design`. Its first page is what the documents compose with, the working order (jobs \u2192 tables and fields \u2192 `records` \u2192 one app per job \u2192 one `set_tables` call with every table, then every `set_app` call) and the section addresses; every page of it is whole. Every key of a table document and an app body, every field `type` with the config each one needs, the option / view / inline-template shapes, `records` (how a row of a table is recognised), `write_rules`, the apps (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `\"<table-ref>:<row-ref>\"`), the rules, the caps of one `set_tables` call, and a worked example; it points at `https://lotics.ai/presets/index.json` for example files of several trades, which an agent reads and passes to `set_tables`. **Offline, no account.** |\n| `lotics report '<json>'` \\| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` \u2014 invoking it IS the consent that passive collection needs an opt-in for \u2014 but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** \u2014 a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |\n\n";
1780
+ var cli_reference_default = "# @lotics/cli \u2014 CLI Command Reference\n\nPer-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. Start at [AGENTS.md](../AGENTS.md) for the model this reference assumes; `lotics --help` is the authoritative, always-current verb list.\n\n| Command | What it does |\n|---|---|\n| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (\xA7 COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics app --help`, `lotics file download --help`); `lotics report --help` prints the report frame. |\n| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open \u2014 `https://lotics.ai/cli_login/<request_id>`, also mailed \u2014 and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential \u2014 `docs` among them \u2014 claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |\n| \u2014 | **A flag is refused on a verb that does not read it**, naming the verbs that do, before any request or credential: `--check` on `lotics run` would otherwise be parsed and ignored, and the write it was meant to stop would run. `--json`, `--workspace`, `--api-key` and `--timeout` are read by every verb. |\n| `lotics auth api-key [key]` | `whoami` \u2192 **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth web` | Send a magic link email to access the web app (requires auth) |\n| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key \u2014 read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |\n| `lotics auth logout [<name\\|id>]` | In a pinned dir: delete the local pin. Else: remove the profile (default the active org), `--all` for every one. What happens server-side depends on which KIND of credential it is. A **sign-in** (`auth login` / `auth signup`) is revoked \u2014 logging that terminal out ends its credential rather than leaving a live one behind; a server that cannot be reached, or a credential already dead, never blocks the local forget, and one line names the org and Settings \u2192 Security \u2192 *Keys and terminals*. An **API key** (`auth api-key`) is only forgotten here \u2014 an admin issued it and it is routinely on a server and on other machines, so one terminal signing out must not kill it for everyone; the line says it is still active and names both pages, because Settings \u2192 API keys is admin-only and the credential may well be the holder's own sign-in, which they revoke themselves at Settings \u2192 Security \u2192 *Keys and terminals*. A profile saved before the kind was recorded states nothing, so the SERVER is asked (`auth whoami`) and it is revoked only if the answer is a sign-in: an older server, a credential minted before the column, and a request that fails all leave it alone. |\n| \u2014 | **A refused credential says which of three ways it is dead, and names the remedy that ends its kind.** `This credential expired.` / `was revoked.` / `belongs to a member who is no longer active in this organization.` carries `Run \\`lotics auth login <email>\\` to sign in again.` for a sign-in and `Ask an admin for a new API key (Settings \u2192 API keys).` for an issued key. A credential minted before that was recorded still gets BOTH in one sentence, because nothing on the row tells them apart \u2014 so a headless box is never sent looking for a browser alone. A key the server does not recognize at all gets one flat `Invalid or disabled API key.` \u2014 deliberately, so a guessed key learns nothing, not even that it named a row. The body carries `reason` for a script to branch on, since the code stays `unauthorized` for every 401. |\n| `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |\n| `LOTICS_ORG=<name\\|id>` | Scope every command in this shell to one saved org. **Resolved once, before any command dispatches**, so a value matching no saved credential refuses every verb with one sentence \u2014 a read, a write, and a local check that needs no credential alike \u2014 and refuses it before the first byte is written. It refuses even when a credential arrives another way, because `--api-key` / `LOTICS_API_KEY` outrank it in the precedence chain and a write must never fall through to whatever THOSE name while the variable says otherwise; when the variable resolves and a key is also given, the key decides and the command says so. The refusal lists the orgs this machine holds, so it is answerable without another command (`lotics org` is refused by the same rule). A name is whatever the credential was SAVED under \u2014 a server-side rename never moves it, and the new name resolves too, so both keep working and `lotics org` prints the pair. |\n| `lotics org use <name\\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous \u2192 error) or id. No flag \u2192 global `active_org`; `--local` \u2192 a `.lotics/config.json` pointer in the current dir. |\n| `lotics workspace` | List workspaces in the active org, marks current with `(current)` |\n| `lotics workspace select <id>` | Set the workspace in the **active scope** \u2014 a local pin if the dir has one, else the active org's global profile. Records the workspace's NAME beside its id, which is what the `lotics \u2192 <org> / <workspace>` echo prints; `workspace list`, `workspace create`, `workspace rename` and `org use` record it too, so a target is named rather than identified. Until one command has listed it, the echo prints the id and says the name is not known yet. |\n| `lotics workspace create <name> [--timezone <Area/City>] [--currency <ISO>]` | Create a new workspace (admin only), auto-switches to it. Neither flag is defaulted from THIS machine, unlike signup: an extra workspace is routinely created by an operator for somebody else. Without `--timezone` the new workspace inherits the zone of the org's OLDEST workspace; without `--currency` it takes the org's default. Both ride the create, so the workspace is never briefly denominated in a currency nobody asked for. `--currency` takes an ISO-4217 code (case-insensitive; anything else is refused). |\n| `lotics workspace rename <name>` | Rename the **current** workspace (admin only) \u2014 the endpoint takes its target from the request's workspace, never a path id, so switch with `workspace select <id>` first and read the `lotics \u2192 <org> / <workspace>` echo before trusting it \u2014 both halves are names, and the rename moves the cached one in the same act. Carries the workspace's existing `default_currency` and `timezone` through unchanged: the endpoint takes the whole settings triple, so sending only a name would blank the other two. |\n| `lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]` | Change the CURRENT workspace's name, default currency or timezone \u2014 `PATCH /v1/workspace`, admin only. Only what you name changes; the endpoint takes the whole triple, so the CLI carries the two you did not. `rename` is this verb with the name alone, which is why it can never forget the other two. Both values are invisible once they are wrong: the currency decides how every money field RENDERS and the zone decides how every date BUCKETS, on a workspace whose whole purpose may be to look like the customer's own. `--json` prints the updated workspace. |\n| `lotics workspace delete <id> --yes` | Delete a workspace by id (admin only). **Soft delete** \u2014 `archived_at` is set, so it drops out of listings, can no longer be selected, and its tables/records go dark, while the data is retained and recoverable. Its **apps are cascade-archived** too \u2014 every app entry point (embedded, public link, standalone subdomain, incl. anonymous public links) stops serving. Refuses the org's **only** active workspace (400) and any workspace outside the caller's org (404). Requires `--yes` to confirm (destructive; the CLI is used non-interactively). |\n| `lotics workspace doctor` | Report workspace-wide dangling schema references via `GET /v1/workspaces/dangling-references` \u2014 every active app/workflow artifact whose prefixed schema id no longer resolves, printed as `<referent.kind> \"<name>\" (<id>) \u2192 <namespace> <id> (missing)`; healthy prints a one-line all-clear. **Exits non-zero (exit 1) on findings** so scripts can gate on it. Resolves the first workspace like every data command (runs before the global workspace resolution). Admin-only. |\n| `lotics tools` | List tools by category with descriptions |\n| `lotics tools <name>` | Full description, input JSON Schema, and the output schema where the tool answers a shape, for one tool; a workflow step says to call it from a workflow body, not `lotics run` |\n| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \\| lotics run <tool> -`) \u2014 both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone \u2014 the error names both escapes. |\n| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |\n| `lotics run <tool>` \u2014 **file cells** | A file in a tool's result carries its `fil_\u2026` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file \u2014 this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |\n| \u2014 | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_query`) and every one that builds: `set_tables` (a list of table documents), `set_app` (one app), `set_app_queries`, `set_app_workflow`. A command exists only for work that touches a local file: `app apply`, `app create --custom`, `app pull`, `app deploy`. |\n| \u2014 | **An app's queries and workflows take more than their params and inputs** (`lotics docs agent_surface`). `run_app_query '{\"app_id\",\"alias\",\"params\"}'` also takes `filters` and `sort` over the query's output columns, `fields` (the columns to return), `count` with `count_by` or `count_where`, `aggregate`, `limit` (200 rows at most; 50 where none is stated) and `cursor`, and `runs` \u2014 up to 20 reads in one call, each `{\"alias\", \u2026}`, a refused one answered with its `error`; one call's rows total at most 262,144 bytes. `export: {\"key\", \u2026narrowing}` instead of the read arguments makes the file an app's Export makes (`lotics docs agent_surface`). `run_app_workflow '{\"app_id\",\"alias\",\"inputs\"}'` also takes `dry_run: true` \u2014 a rehearsal on the real inputs, nothing written or sent; the answer is what the run would return, with the reads it made under `dry_run.reads`, the calls it would make under `dry_run.writes` and the papers it rendered under `dry_run.papers` and the parts of the answer it could not say under `dry_run.unpredicted`; a workflow no dry run is offered of answers `status: \"not_previewed\"` \u2014 and `runs` \u2014 up to 50 runs in order, the first refused, failed or not previewed ending the call, an input `{\"from_run\": <index>, \"path\": \"data.<key>\"}` reading an earlier run's answer. |\n| \u2014 | **The exit code reports the WORK, not just the call \u2014 for the tool that RUNS one.** `run_app_workflow` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`, or `not_previewed` \u2014 a dry run asked for and not given) exit non-zero and print `<tool> \u2192 <status>: <message>` to stderr, so `lotics run \u2026 && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE \u2014 an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |\n| `lotics run <tool> '<json>' -o <dir>` | Also save every **signed link** the answer carries \u2014 an object with a `url`, a `filename` and the `expires_at` its signature ends at, at any depth, such as `screenshot_app`'s images. Each is fetched into `<dir>` (created if missing) under its `filename`, taking a free spelling beside a file of that name already there, and a link's `snapshot` text is written beside it as `<name>.txt`. The written paths follow the answer on **stdout**, one per line (on stderr under `--json`, whose stdout stays the one object). A link is signed for an hour; one the store no longer serves exits non-zero. A workspace file states no `expires_at` and is left to `lotics file download`. |\n| `lotics run set_member '{\"members\": {\"<email or member id>\": {\"role\": \"member\", \"name\": \"\u2026\"}}}'` | Invite, rename, remove (`null`) or restore members, admin only: the whole map is checked before an email is sent. A member's `lotics tools` does not list it, nor `set_group` or `set_workspace`. View-as is the web app's preview alone; the CLI acts as the signed-in member. |\n| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |\n| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes \u2014 harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |\n| `lotics file upload <file\\|dir...>` (alias `lotics upload`) \xB7 `--stdin` \xB7 `--base64` \xB7 `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files`, several such files to a request up to half that route's 100 MiB cap; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose \u2014 one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** \u2014 an attachment decoded in memory, a generated document, a signed download link \u2014 each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY \u2014 `Buffer.from(s, \"base64\")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |\n| `lotics file download <file_id> [<path>]` \xB7 `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` \u2192 fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there \u2014 the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once \u2014 a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_alias> [-o <dir>]` (the field named by the alias `get_record` shows it under) spreads every file on a record's file field over a DIRECTORY \u2014 there is no single file for N files to be. |\n| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first \u2014 id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset \u2014 pass it back as given \u2014 so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to \"what is in here\" short of reading Postgres. |\n| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. Deleting an original deletes every version of it; a file's current version cannot be deleted on its own, and a document template's file, or a version of it, only by a member who manages the template. **Refused while a record cell, a comment, a knowledge doc, a document template, a voice session or a pending change request still references the file or a version it takes along** \u2014 the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what \"deleted\" means here. There is no `lotics delete`: the verb needs its noun. |\n| `lotics knowledge list [--include-hidden \\| --deleted]` | `GET /v1/knowledge_docs` \u2014 a table of id, name, tags, description (`--json` for the docs). **REST, not `get_workspace` kind `knowledge`**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. **`--deleted` is the one tool-backed form**: it calls `get_workspace` with `kind: \"knowledge\"` and `include_archived: true`, page by page, and prints the deleted docs you can manage (id, name, deleted time, description); it cannot be combined with `--include-hidden`. |\n| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \\| --content <str>)` | Read the body client-side (a file XOR an inline string \u2014 exactly one required), then call `set_knowledge` with `{ name, description, content, tags? }` and no `knowledge_doc_id`, which makes a doc (description defaults to `\"\"`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |\n| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) \u2192 the doc with its **hydrated `content`** (the one whole-content read for a non-sandbox client; the tool `read_knowledge` reads a line range). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |\n| `lotics knowledge update <id> [--from <file.md> \\| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `set_knowledge` with a `knowledge_doc_id` and **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set \u2014 the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |\n| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` \u2014 one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |\n| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** \u2014 the Library's list, and the corpus `get_workspace` lists and its `search` searches \u2014 while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as \"everything\". |\n| `lotics knowledge rm <id...>` | Delete docs via `delete_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a live doc you manage that no app or workflow names, or nothing is deleted. Sharing and history are kept. The REST execute path does not gate `needsApproval`, so this runs unattended. |\n| `lotics knowledge restore <id...>` | Bring deleted docs back via `restore_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a deleted doc you manage, or nothing is restored. Refuses the no-argument form. |\n| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty \u2014 before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`GET /v1/apps/{id}/types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: a JSON app is made by `lotics app apply` (the tool `set_app`). The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |\n| `lotics app pull [app_id] [path]` | **An app's live definition, pulled.** The app is read with `get_app`: its author is shown a body, a JSON **tree** (with the addresses of its nodes) pulled as that tree and a custom-code app (the identity alone) as its source; an app whose stored tree cannot be read is refused with the reason `get_app` gives, and so is an app that shows the caller no body (the caller does not author it, or it has no version). **A JSON app**: the tree is written as JSON to `[path]` (a directory receives `./<name>.app.json`; default `./<name>.app.json`), and the app's id and current version beside it in `<file>.lotics.json` (`{ \"app_id\": \"\u2026\", \"version_id\": \"\u2026\", \"digest\": \"\u2026\" }`, the digest being the file as the CLI last left it; a record without all three is refused by `app apply`, naming `lotics app pull`) \u2014 a custom-code project records both in `package.json#lotics`, a single file has no such place \u2014 so the next `lotics app apply <file>` edits that app, and is refused if the app has since moved past that version. An existing file is overwritten only when its `.lotics.json` names this same app and the file is unchanged since it was last pulled or applied; a file with edits, or whose record names the app but no digest, is refused, naming `--force`, which overwrites it. `--force` is a flag of `app pull` alone. `--json` prints `{workspace_id, app_id, path, outcome}`. **A custom-code app**: its live source, as a project ready to deploy on it. The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** \u2014 but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`GET /v1/apps/{id}/types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). |\n| `lotics app apply <file> [--app <app_id>] [--check] [--json]` | **A JSON app, one at a time, through the tool `set_app`.** The file is an app's tree (`lotics docs reference`: the App node with the nodes under it, its tables and templates named by alias). **No app known** (no `--app`, no `<file>.lotics.json` beside it): the file is sent as `{ \"body\": \u2026 }` to make a new app, named by the body's `name`, and the id made is printed, then recorded with its version in `<file>.lotics.json`. **An app known**: its current tree is read (`get_app`) and the **RFC 7396 merge patch** that turns it into the file is sent as `body` with the `app_id` and the version the patch was computed against as `base_version_id` \u2014 a key the file drops is `null` in the patch, a list is replaced whole, what the file leaves alone is left alone, and a tree the app already has sends nothing and says so. **A file pulled at a version the app has since left is refused** before anything is sent, naming both versions and `lotics app pull <app_id> <path>`: pull the current tree into another path, carry the edits over, apply that file. `--app` naming an app other than the sibling file's is refused too. A write that races the apply is refused by the server, which names the app's current version. A file that sets an object key to `null` is refused naming the key, since a merge patch reads `null` as a removal. `--app` names the app, else the sibling file does; an app whose `get_app` shows no body (it is not yours) or whose stored tree cannot be read is refused, and a custom-code app takes its identity (name, description, icon, colour) only, and no `<file>.lotics.json` records it, since an identity write makes no version \u2014 its source deploys with `app deploy`. **The file is never rewritten**: a node the tree leaves out draws as the rows call for it and is stored nowhere (`lotics docs design`). It prints what `set_app` answered and reports its `outcome`: its notes and findings, every finding at once, and the saves, adds and Deletes the write grants and removes. **`--check` sends `dry_run`** (a flag of `app apply` alone, as is `--app`) \u2014 what would refuse the tree, and what it would grant and remove \u2014 and writes nothing, the file and its sibling included; an `outcome` of `refused` exits 1. A tree the app already has, or one `set_app` answers `unchanged`, records the version the app stays at in the sibling. **To go back to an earlier version, apply the earlier tree again.** Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, app_id, outcome, write}`, `outcome` being `set_app`'s: `created`, `updated`, `unchanged` or `refused` (with `--check`, what it would be). |\n| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`GET /v1/apps/{id}/types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent; a type error in the app's own files stops the deploy, one inside an installed package is reported and does not) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged \u2014 those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |\n| `lotics docs` \\| `lotics docs <area>[/<section>]` \\| `lotics docs [<area>[/<section>]] --grep <text>` | **This CLI's own references, carried inside the binary** \u2014 the model reference, this index, and every doc under `docs/` \u2014 so the doc a reader opens describes the binary answering, listed by the job a reader comes to do. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. **A reference the binary's copy lacks, or a part of one the server serves, is read from the server's `docs` tool** with this machine's credential, after the copy's refusal \u2014 a page the server added since this binary was built, which a server refusal can cite. A name matching more than one reference, or a part missing from a guide to this CLI, is answered by the copy alone. **`--grep` searches the references the server serves** (not this CLI's own guides, which it refuses), with this machine's credential, narrowed to the area or section named: literal text unless `--regex`; a literal search that finds nothing while the pattern read as an expression matches (`a|b`) returns those lines under a note saying so, and `--literal` keeps the literal zero; with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` \u2014 the dialect the knowledge lines of `get_workspace` with `search` are read in \u2014 each hit under the address that opens its page; any of those options without `--grep` is refused. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |\n| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script \u2014 the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe \u2014 a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |\n| `lotics docs reference` \\| `lotics docs reference/<section>[/\u2026]` | **The reference for tables and apps, from inside the binary** \u2014 at this CLI's version; `lotics docs` lists it under \"Build an app\", after `design`. Its first page is what the documents compose with, the working order (jobs \u2192 tables and fields \u2192 `records` \u2192 one app per job \u2192 one `set_tables` call with every table, then every `set_app` call) and the section addresses; every page of it is whole. Every key of a table document and an app body, every field `type` with the config each one needs, the option / view / inline-template shapes, `records` (how a row of a table is recognised), `write_rules`, the apps (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `\"<table-ref>:<row-ref>\"`), the rules, the caps of one `set_tables` call, and a worked example; it points at `https://lotics.ai/presets/index.json` for example files of several trades, which an agent reads and passes to `set_tables`. **Offline, no account.** |\n| `lotics report '<json>'` \\| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` \u2014 invoking it IS the consent that passive collection needs an opt-in for \u2014 but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** \u2014 a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |\n\n";
1781
1781
 
1782
1782
  // docs/data_model.md
1783
- var data_model_default = '# The data model \u2014 tables, their fields, and how they relate\n\nThe decisions here outlive any one app, and most become expensive the moment a second screen depends\non them. They stand apart from building an app on purpose: **every workspace starts with tables\nand many never get an app**, so schema design is not a chapter of app building.\n\nThe rules below share one signature. **Both sides read correctly on their own**, so nothing reports\nthe problem \u2014 no error, no empty column, no failing query. Each is found by looking for it. Then,\nunder **Fields**, what each field type takes.\n\n## One fact, one column\n\n**Read the table\'s existing fields first (`get_table`), and add nothing that stores a fact the table already\nstores.** A value written as text and the same value held as a `link` are one fact in\ntwo columns \u2014 a customer\'s city typed into a text box beside a link to the city record, a status\nword beside the select that decides it, a total beside the formula that computes it.\n\nTwo columns for one fact do not stay equal. Some writer sets only one of them, and nothing reports\nthe divergence: both rows still look correct on their own. A reader that then matches on the text\nhalf treats "Acme" and "Acme Ltd" as different records, so an import creates a duplicate every time\nit runs.\n\n- **Prefer the link, the select, or the formula.** Text is a RENDERING of a record; compose it when\n you read, rather than storing it a second time.\n- **Renaming, retyping or re-pointing the existing field beats adding another.** A field\'s key is\n stable, so a rename breaks nothing that addresses it by key.\n- **Superseding a field means DELETING it**, not leaving it beside its replacement with a\n description that says which one is real.\n- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a\n real distinction \u2014 read what it MEANS before removing it.\n- **A stored field becomes required only once every row holds a value**, so it always holds one: fill the\n empty cells first, then set it. A computed field\'s flag is checked as each row is written.\n\n## One entity, one table \u2014 and the test is measurable\n\nVariation belongs in a column \u2014 a multi-select role, a kind, a stage \u2014 not in a second table. Two\ntables for one kind of thing give the same real-world entity two rows, two ids and two halves of its\nhistory, and each screen shows whichever half it happens to link to.\n\nSplit tables are often right. A supplier book beside a customer book is a normal shape, and merging\non suspicion is a large repoint bought for nothing. So do not argue it in the abstract:\n\n> **List both tables\' names and look for one that appears in both.**\n\nNone means the split is holding. One means it has broken \u2014 the usual cause is a party you begin to\ninvoice as well as buy from \u2014 and the fix is to merge before a second screen depends on the copy.\n\n## One vocabulary wherever values are COPIED between tables\n\nTwo `select` fields for one concept carry different option keys even when their labels match. Copy an option by its\nalias or label: the record tools match an option by alias or label, so a value copied by alias lands\non the option of that alias on the other field. Prefer a link to a shared reference table where the set is open or\ngrowing. Where one side holds more values than the other, the wider side must **refuse** what the narrower one cannot\nexpress rather than quietly picking the nearest value.\n\nA value read through a link or a lookup reads exactly as the same field would on its own row: an option written by its alias in\na body and read as its key, a member or a link by its id. A value a check matches\nacross tables is a link to one table: two selects never compare, and a save that compares them is refused.\n\nA lookup reads as one flat list of the values it looks up, in a check and in every body: `first(record.x)` is the one value through a one-row link and `includes(record.x, v)` asks whether any row holds `v`; two selects never compare, so a value matched across tables is a link to one table. A single member, a single select, a number or a text reads as the value itself, and `first(...)` of one is refused when the body is saved. A rollup that adds up (a sum, a count) over no rows is 0, never empty, as `sum` over an empty list is; a min, a max or an average over no rows is empty.\n\n## A copy boundary accounts for EVERY source field\n\nEach field on the source gets a column on the destination, a deliberate drop with the reason written\ndown, or a refusal.\n\nA field with nowhere to land is data destroyed at the boundary, and it is invisible afterwards: the\ndestination is not empty and not obviously wrong \u2014 just a number that no longer agrees with where it\ncame from.\n\n## Provenance is a LINK, not a flag and not a copy\n\nA row created BY another row carries a link to it.\n\nThat link is what makes "is this the estimate or the actual", "where did this come from" and "have we\nalready imported this" answerable at all. A boolean records that something was true once; a link\nstays true, survives a rename, and lets the next write UPDATE the original instead of adding a second\nrow beside it.\n\n## Say what makes two rows the SAME row\n\nState it as the table\'s `natural_key` (`model/write-rules`); a text key is also `unique`.\n\nAnything that imports, reconciles or de-duplicates has to decide identity, and with no declared key it falls back to\ncomparing displayed text \u2014 which is how one company arrives three times under three spellings. Name the key: a reference\nnumber, a tax id, a link plus a period. Then match on record ids and option aliases, never on rendered labels.\n\n## A stage\'s moment is a date on the row\n\nWho changed what, and when, is the platform\'s audit trail, which an agent already reads (`query_audit_logs`) under\nits own access to the table \u2014 so it is never a table of yours, and an app shows none of it: a history an app shows is\ntable-backed (`records.status.dates`, `records.status.history`, an event table an act makes).\n\nWhen the business reports, filters or prints the moment a row reached a stage (completed on, cancelled on), that\nmoment is a `date` field of the row, as `completed_at` and `canceled_at` are in SQL. State it as `records.status.dates`\n(`model/status`): option \u2192 date. The record write path stamps it as a row is made at the option or moved into it,\nwhoever writes, so no one has to remember; a value the same write states wins, which loads past data as it happened. The\nplatform never clears a stamp: each date is when the row last entered the option, and leaving keeps it, and putting a\nchange back restamps nothing. A date range is a span, never a moment, so it is refused. It is the one way a status is\ntimed: a stage\'s `within` runs from its date, a straight-line process is one select whose options are its steps, each\ndated, and the stage is never read off a run of dates.\n\n`records.status.history` is for a business that reports on every move \u2014 moves per person, or time in a stage a row\nre-enters \u2014 and the platform appends one row per move. An event that carries facts of its own (a reason, an amount, a\nsignature, an inspection\'s findings) is its own table with those fields, made by the act that records it.\n\n## Keep derived chains shallow\n\nFormulas and rollups are computed and STORED when a row is written, and one that reads another recomputes with it. A\nrollup over a formula over a formula is paid three times on every touch, and again for every row upstream of it.\n**Depth costs more than row count**: chain depth is yours to keep down, row scanning is the platform\'s.\n\n## Fields\n\nA field is stated inside its table\'s document in a `set_tables` call, keyed by alias. Every field `type`, the\nkeys each one takes (notation, defaults, options, links, formulas, rollups, lookups, copies) and a worked\nexample are in `docs path="model/field"`; this page holds the choices around them.\n\n### Types and formats\n\n`type` is one of `text`, `number`, `date`, `boolean`, `select`, `member`,\n`link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`.\n\nA URL, an email, markdown, a checkbox, a datetime, money, a quantity or a percentage is a `format`\nor a `notation` on another type, never a `type`:\n\n| Wanted | Field |\n|---|---|\n| URL or external link | `{ type: "text", format: "link" }` |\n| Email or phone | `{ type: "text" }` |\n| Markdown | `{ type: "text", format: "markdown" }` |\n| Checkbox | `{ type: "boolean" }` |\n| Money | `{ type: "number", notation: { style: "currency", currency: "USD" } }` |\n| Quantity | `{ type: "number", notation: { style: "unit", unit: "kg" } }` |\n| Percentage | `{ type: "number", notation: { style: "unit", unit: "percent" } }` |\n| Datetime | `{ type: "date", format: "datetime" }` |\n| Date range | `{ type: "date", format: "date_range" }` |\n\n### Changing a field\n\nRead the table with `get_table`, which answers its document exactly as `set_tables` takes it, then send `set_tables` with\nthe `table_id` and only what changes. The table is edited by an RFC 7396 merge patch of its document, so what the patch\nleaves alone is left alone and `null` deletes the field it names. Every key a table and a field take is in\n`docs path="model/tables"` and `model/field`.\n\nA table is named by its alias wherever a call names one (`table_id`, a link\'s `table`, a lookup, `records.history`), and a new table in the call is named by its `ref`, which becomes its alias. The alias is kept while the table exists, archived or not: asking for one an archived table holds is refused, offering to restore that table or to choose another alias. A filter in a table document (a rollup\'s `filter`, `options_where`, a colour\'s condition, a check\'s lookup) takes the record tools\' operators and values (`is`, `is_any_of`, `gte`, a plain date as a day). An `autonumber` field takes `next` (`"next": 232`): the number its next row gets, refused below the highest already issued.\n\n- **A list is replaced whole**, so an option cannot be nulled. To rename, recolour, add or remove an option, restate the\n field\'s whole `options` list, each option with its alias (`model/field/select`). A rename changes the label and every\n record keeps its value; an alias left out is removed, which needs `force` while rows hold it.\n- **A number field becomes an `autonumber`** when every row holds its own whole number of 1 or more: the numbers are kept\n and the next row takes the one after the highest (a `next` must be above it). Otherwise the call is refused, saying how\n many rows are blank, repeated or not whole, with up to 5 row ids. No other type converts to or from an autonumber; add\n one as a new field, which numbers the rows already there from 1 (or from `next`) in creation order.\n- **Splitting an option is a rename plus an add.** The old option keeps its alias and takes the first part\'s label, so its\n rows and every app reference to it keep working; the second part is a new option. Rows that belong to the second part\n are then moved by a filtered `update_records`.\n- **A status history holds every option its status ever had** and follows them in the same write: an option added or renamed\n on the status is added or renamed on the history, which a call never restates. Removing an option on the status leaves it\n on the history, because the moves already written hold it. A call that changes nothing of the status\'s options leaves the\n history alone, and options only the history holds stay. Deleting a row deletes its history rows with it, and restoring\n the row restores them; they never hold a delete back.\n- A link names its table by id, or by the `ref` of another table of the same call, and states its `multi`. A two-way link\n states `pair` on one table, as `true` or as a free alias, and the platform makes the partner on the other table with the\n inverse cardinality; a many-to-many link states both sides, since the inverse of many is not decided.\n A link already holding rows is paired the same way, with its partner stated beside it in the call: the partner is filled from\n the links the rows hold and no cell of the link is rewritten. `pair: null` lets a pair go, both fields staying one-way links\n with their values; a field that holds one row, the partner or the link itself, refuses a pair two rows would crowd, naming the rows.\n The partner may stand on a table the call does not name: `pair: "<alias>"` makes it there, or pairs the one-way link that table\n already holds under that alias, and the answer lists that table among the call\'s. Only someone who manages that table may.\n- A formula\'s `output_type` is inferred from its expression; state it only to override.\n- **A call\'s removals are judged together, on what the call leaves.** A table and the links and lookups naming it, removed in one call, pass; every reader left outside the call is named in one answer, not one per retry. An app reading a field or an option that leaves is rebuilt in the same call, and the answer (a dry run too) lists each node of its tree that changed: a column, a filter, a sort key or a field of a part that only drew the field is dropped, and an option leaves every list of options it stood in. What has no meaning without it refuses the call, naming the node: an act that sets a removed option, a part left with nothing to draw, a list that held only the removed option. An app drawing a field that is retyped, or a link pointed at another table, is rebuilt too, its tree unchanged: where the field no longer lets the tree draw what it drew, the call refuses, naming the node, and a field that became a formula, rollup, lookup or autonumber leaves the app\'s allowlist of saves. A workflow body its author wrote is never rewritten: it is checked again against the tables as the call leaves them, and the call refuses, naming the workflow, only if it no longer checks. A table\'s check or workflow that reads a retyped field or a link pointed elsewhere is judged the same way and refuses, naming it, only if it no longer holds. Deleting one field through the table\'s field route drops no node: an app whose tree draws the field refuses it, naming the node.\n- **A field something reads cannot go.** A query, a workflow, a check, an app only its owner can rebuild, or another\n table\'s formula, rollup, lookup or copy that reads the field refuses its deletion, naming the reader. Change or remove\n the reader first, or in the same call.\n- **`force` consents to losing values:** deleting a field rows hold, removing an option rows hold, and a change of type\n that does not convert every value.\n- A money formula is proven by its numbers. `aggregate_records` the affected totals before the change and after, and\n compare. A new field in a formula is tested for the value you want, never for differing from an empty cell, which reads\n `""` and differs from every option.\n- A check\'s expressions are written in the language of `workflows/table-checks`.\n- `views` are stated when the table is created; afterwards they change in chat.\n- A refused edit writes nothing and lists everything wrong; `dry_run` is for reading the grants it would make or deciding `force`.\n\nA worked call: add the option `cancelled` to `stage` (the whole list restated), add a check, and consent to losing\nwhat rows hold.\n\n```json5\n{\n "tables": [\n {\n "table_id": "<id of the table>",\n "fields": {\n "stage": {\n "options": [\n { "alias": "draft", "label": "Draft" },\n { "alias": "confirmed", "label": "Confirmed" },\n { "alias": "cancelled", "label": "Cancelled", "color": "red" }\n ]\n }\n },\n "checks": {\n "returns_after_leaves": {\n "on": ["create", "update"],\n "when": "!isNull(record.returns)",\n "fail_when": "record.returns < record.leaves",\n "message": "\\"Return before departure\\"",\n "field": "returns"\n }\n }\n }\n ],\n "force": true,\n "dry_run": true\n}\n```\n\nSplitting `queued` into waiting and underway is one call of the same shape:\n\n```json5\n{ "tables": [{ "table_id": "<id of the table>", "fields": { "stage": { "options": [\n { "alias": "draft", "label": "Draft" },\n { "alias": "queued", "label": "Waiting" },\n { "alias": "underway", "label": "Underway", "color": "green" }\n] } } }] }\n```\n\n`table_id` picks the table patched; `fields` merges by alias, so only `stage` changes; `checks` adds one under its alias\n(`null` removes it); `force` consents to losing values (leave it out unless rows hold a removed option); `dry_run`\nchecks and writes nothing, for reading the grants or deciding `force` before the call that writes.\n';
1783
+ var data_model_default = '# The data model \u2014 tables, their fields, and how they relate\n\nThe decisions here outlive any one app, and most become expensive the moment a second screen depends\non them. They stand apart from building an app on purpose: **every workspace starts with tables\nand many never get an app**, so schema design is not a chapter of app building.\n\nThe rules below share one signature. **Both sides read correctly on their own**, so nothing reports\nthe problem \u2014 no error, no empty column, no failing query. Each is found by looking for it. Then,\nunder **Fields**, what each field type takes.\n\n## One fact, one column\n\n**Read the table\'s existing fields first (`get_table`), and add nothing that stores a fact the table already\nstores.** A value written as text and the same value held as a `link` are one fact in\ntwo columns \u2014 a customer\'s city typed into a text box beside a link to the city record, a status\nword beside the select that decides it, a total beside the formula that computes it.\n\nTwo columns for one fact do not stay equal. Some writer sets only one of them, and nothing reports\nthe divergence: both rows still look correct on their own. A reader that then matches on the text\nhalf treats "Acme" and "Acme Ltd" as different records, so an import creates a duplicate every time\nit runs.\n\n- **Prefer the link, the select, or the formula.** Text is a RENDERING of a record; compose it when\n you read, rather than storing it a second time.\n- **Renaming, retyping or re-pointing the existing field beats adding another.** A field\'s key is\n stable, so a rename breaks nothing that addresses it by key.\n- **Superseding a field means DELETING it**, not leaving it beside its replacement with a\n description that says which one is real.\n- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a\n real distinction \u2014 read what it MEANS before removing it.\n- **A stored field becomes required only once every row holds a value**, so it always holds one: fill the\n empty cells first, then set it. A computed field\'s flag is checked as each row is written.\n\n## One entity, one table \u2014 and the test is measurable\n\nVariation belongs in a column \u2014 a multi-select role, a kind, a stage \u2014 not in a second table. Two\ntables for one kind of thing give the same real-world entity two rows, two ids and two halves of its\nhistory, and each screen shows whichever half it happens to link to.\n\nSplit tables are often right. A supplier book beside a customer book is a normal shape, and merging\non suspicion is a large repoint bought for nothing. So do not argue it in the abstract:\n\n> **List both tables\' names and look for one that appears in both.**\n\nNone means the split is holding. One means it has broken \u2014 the usual cause is a party you begin to\ninvoice as well as buy from \u2014 and the fix is to merge before a second screen depends on the copy.\n\n## One vocabulary wherever values are COPIED between tables\n\nTwo `select` fields for one concept carry different option keys even when their labels match. Copy an option by its\nalias or label: the record tools match an option by alias or label, so a value copied by alias lands\non the option of that alias on the other field. Prefer a link to a shared reference table where the set is open or\ngrowing. Where one side holds more values than the other, the wider side must **refuse** what the narrower one cannot\nexpress rather than quietly picking the nearest value.\n\nA value read through a link or a lookup reads exactly as the same field would on its own row: an option written by its alias in\na body and read as its key, a member or a link by its id. A value a check matches\nacross tables is a link to one table: two selects never compare, and a save that compares them is refused.\n\nA lookup reads as one flat list of the values it looks up, in a check and in every body: `first(record.x)` is the one value through a one-row link and `includes(record.x, v)` asks whether any row holds `v`; two selects never compare, so a value matched across tables is a link to one table. A single member, a single select, a number or a text reads as the value itself, and `first(...)` of one is refused when the body is saved. A rollup that adds up (a sum, a count) over no rows is 0, never empty, as `sum` over an empty list is; a min, a max or an average over no rows is empty.\n\n## A copy boundary accounts for EVERY source field\n\nEach field on the source gets a column on the destination, a deliberate drop with the reason written\ndown, or a refusal.\n\nA field with nowhere to land is data destroyed at the boundary, and it is invisible afterwards: the\ndestination is not empty and not obviously wrong \u2014 just a number that no longer agrees with where it\ncame from.\n\n## Provenance is a LINK, not a flag and not a copy\n\nA row created BY another row carries a link to it.\n\nThat link is what makes "is this the estimate or the actual", "where did this come from" and "have we\nalready imported this" answerable at all. A boolean records that something was true once; a link\nstays true, survives a rename, and lets the next write UPDATE the original instead of adding a second\nrow beside it.\n\n## Say what makes two rows the SAME row\n\nState it as the table\'s `natural_key` (`reference/write-rules`); a text key is also `unique`.\n\nAnything that imports, reconciles or de-duplicates has to decide identity, and with no declared key it falls back to\ncomparing displayed text \u2014 which is how one company arrives three times under three spellings. Name the key: a reference\nnumber, a tax id, a link plus a period. Then match on record ids and option aliases, never on rendered labels.\n\n## A stage\'s moment is a date on the row\n\nWho changed what, and when, is the platform\'s audit trail, which an agent already reads (`query_audit_logs`) under\nits own access to the table \u2014 so it is never a table of yours, and an app shows none of it: a history an app shows is\ntable-backed (`records.status.dates`, `records.status.history`, an event table an act makes).\n\nWhen the business reports, filters or prints the moment a row reached a stage (completed on, cancelled on), that\nmoment is a `date` field of the row, as `completed_at` and `canceled_at` are in SQL. State it as `records.status.dates`\n(`reference/status`): option \u2192 date. The record write path stamps it as a row is made at the option or moved into it,\nwhoever writes, so no one has to remember; a value the same write states wins, which loads past data as it happened. The\nplatform never clears a stamp: each date is when the row last entered the option, and leaving keeps it, and putting a\nchange back restamps nothing. A date range is a span, never a moment, so it is refused. It is the one way a status is\ntimed: a stage\'s `within` runs from its date, a straight-line process is one select whose options are its steps, each\ndated, and the stage is never read off a run of dates.\n\n`records.status.history` is for a business that reports on every move \u2014 moves per person, or time in a stage a row\nre-enters \u2014 and the platform appends one row per move. An event that carries facts of its own (a reason, an amount, a\nsignature, an inspection\'s findings) is its own table with those fields, made by the act that records it.\n\n## Keep derived chains shallow\n\nFormulas and rollups are computed and STORED when a row is written, and one that reads another recomputes with it. A\nrollup over a formula over a formula is paid three times on every touch, and again for every row upstream of it.\n**Depth costs more than row count**: chain depth is yours to keep down, row scanning is the platform\'s.\n\n## Fields\n\nA field is stated inside its table\'s document in a `set_tables` call, keyed by alias. Every field `type`, the\nkeys each one takes (notation, defaults, options, links, formulas, rollups, lookups, copies) and a worked\nexample are in `docs path="reference/field"`; this page holds the choices around them.\n\n### Types and formats\n\n`type` is one of `text`, `number`, `date`, `boolean`, `select`, `member`,\n`link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`.\n\nA URL, an email, markdown, a checkbox, a datetime, money, a quantity or a percentage is a `format`\nor a `notation` on another type, never a `type`:\n\n| Wanted | Field |\n|---|---|\n| URL or external link | `{ type: "text", format: "link" }` |\n| Email or phone | `{ type: "text" }` |\n| Markdown | `{ type: "text", format: "markdown" }` |\n| Checkbox | `{ type: "boolean" }` |\n| Money | `{ type: "number", notation: { style: "currency", currency: "USD" } }` |\n| Quantity | `{ type: "number", notation: { style: "unit", unit: "kg" } }` |\n| Percentage | `{ type: "number", notation: { style: "unit", unit: "percent" } }` |\n| Datetime | `{ type: "date", format: "datetime" }` |\n| Date range | `{ type: "date", format: "date_range" }` |\n\n### Changing a field\n\nRead the table with `get_table`, which answers its document exactly as `set_tables` takes it, then send `set_tables` with\nthe `table_id` and only what changes. The table is edited by an RFC 7396 merge patch of its document, so what the patch\nleaves alone is left alone and `null` deletes the field it names. Every key a table and a field take is in\n`docs path="reference/tables"` and `reference/field`.\n\nA table is named by its alias wherever a call names one (`table_id`, a link\'s `table`, a lookup, `records.history`), and a new table in the call is named by its `ref`, which becomes its alias. The alias is kept while the table exists, archived or not: asking for one an archived table holds is refused, offering to restore that table or to choose another alias. A filter in a table document (a rollup\'s `filter`, `options_where`, a colour\'s condition, a check\'s lookup) takes the record tools\' operators and values (`is`, `is_any_of`, `gte`, a plain date as a day). An `autonumber` field takes `next` (`"next": 232`): the number its next row gets, refused below the highest already issued.\n\n- **A list is replaced whole**, so an option cannot be nulled. To rename, recolour, add or remove an option, restate the\n field\'s whole `options` list, each option with its alias (`reference/field/select`). A rename changes the label and every\n record keeps its value; an alias left out is removed, which needs `force` while rows hold it.\n- **A number field becomes an `autonumber`** when every row holds its own whole number of 1 or more: the numbers are kept\n and the next row takes the one after the highest (a `next` must be above it). Otherwise the call is refused, saying how\n many rows are blank, repeated or not whole, with up to 5 row ids. No other type converts to or from an autonumber; add\n one as a new field, which numbers the rows already there from 1 (or from `next`) in creation order.\n- **Splitting an option is a rename plus an add.** The old option keeps its alias and takes the first part\'s label, so its\n rows and every app reference to it keep working; the second part is a new option. Rows that belong to the second part\n are then moved by a filtered `update_records`.\n- **A status history holds every option its status ever had** and follows them in the same write: an option added or renamed\n on the status is added or renamed on the history, which a call never restates. Removing an option on the status leaves it\n on the history, because the moves already written hold it. A call that changes nothing of the status\'s options leaves the\n history alone, and options only the history holds stay. Deleting a row deletes its history rows with it, and restoring\n the row restores them; they never hold a delete back.\n- A link names its table by id, or by the `ref` of another table of the same call, and states its `multi`. A two-way link\n states `pair` on one table, as `true` or as a free alias, and the platform makes the partner on the other table with the\n inverse cardinality; a many-to-many link states both sides, since the inverse of many is not decided.\n A link already holding rows is paired the same way, with its partner stated beside it in the call: the partner is filled from\n the links the rows hold and no cell of the link is rewritten. `pair: null` lets a pair go, both fields staying one-way links\n with their values; a field that holds one row, the partner or the link itself, refuses a pair two rows would crowd, naming the rows.\n The partner may stand on a table the call does not name: `pair: "<alias>"` makes it there, or pairs the one-way link that table\n already holds under that alias, and the answer lists that table among the call\'s. Only someone who manages that table may.\n- A formula\'s `output_type` is inferred from its expression; state it only to override.\n- **A call\'s removals are judged together, on what the call leaves.** A table and the links and lookups naming it, removed in one call, pass; every reader left outside the call is named in one answer, not one per retry. An app reading a field or an option that leaves is rebuilt in the same call, and the answer (a dry run too) lists each node of its tree that changed: a column, a filter, a sort key or a field of a part that only drew the field is dropped, and an option leaves every list of options it stood in. What has no meaning without it refuses the call, naming the node: an act that sets a removed option, a part left with nothing to draw, a list that held only the removed option. An app drawing a field that is retyped, or a link pointed at another table, is rebuilt too, its tree unchanged: where the field no longer lets the tree draw what it drew, the call refuses, naming the node, and a field that became a formula, rollup, lookup or autonumber leaves the app\'s allowlist of saves. A workflow body its author wrote is never rewritten: it is checked again against the tables as the call leaves them, and the call refuses, naming the workflow, only if it no longer checks. A table\'s check or workflow that reads a retyped field or a link pointed elsewhere is judged the same way and refuses, naming it, only if it no longer holds. Deleting one field through the table\'s field route drops no node: an app whose tree draws the field refuses it, naming the node.\n- **A field something reads cannot go.** A query, a workflow, a check, an app only its owner can rebuild, or another\n table\'s formula, rollup, lookup or copy that reads the field refuses its deletion, naming the reader. Change or remove\n the reader first, or in the same call.\n- **`force` consents to losing values:** deleting a field rows hold, removing an option rows hold, and a change of type\n that does not convert every value.\n- A money formula is proven by its numbers. `aggregate_records` the affected totals before the change and after, and\n compare. A new field in a formula is tested for the value you want, never for differing from an empty cell, which reads\n `""` and differs from every option.\n- A check\'s expressions are written in the language of `workflows/table-checks`.\n- `views` are stated when the table is created; afterwards they change in chat.\n- A refused edit writes nothing and lists everything wrong; `dry_run` is for reading the grants it would make or deciding `force`.\n\nA worked call: add the option `cancelled` to `stage` (the whole list restated), add a check, and consent to losing\nwhat rows hold.\n\n```json5\n{\n "tables": [\n {\n "table_id": "<id of the table>",\n "fields": {\n "stage": {\n "options": [\n { "alias": "draft", "label": "Draft" },\n { "alias": "confirmed", "label": "Confirmed" },\n { "alias": "cancelled", "label": "Cancelled", "color": "red" }\n ]\n }\n },\n "checks": {\n "returns_after_leaves": {\n "on": ["create", "update"],\n "when": "!isNull(record.returns)",\n "fail_when": "record.returns < record.leaves",\n "message": "\\"Return before departure\\"",\n "field": "returns"\n }\n }\n }\n ],\n "force": true,\n "dry_run": true\n}\n```\n\nSplitting `queued` into waiting and underway is one call of the same shape:\n\n```json5\n{ "tables": [{ "table_id": "<id of the table>", "fields": { "stage": { "options": [\n { "alias": "draft", "label": "Draft" },\n { "alias": "queued", "label": "Waiting" },\n { "alias": "underway", "label": "Underway", "color": "green" }\n] } } }] }\n```\n\n`table_id` picks the table patched; `fields` merges by alias, so only `stage` changes; `checks` adds one under its alias\n(`null` removes it); `force` consents to losing values (leave it out unless rows hold a removed option); `dry_run`\nchecks and writes nothing, for reading the grants or deciding `force` before the call that writes.\n';
1784
1784
 
1785
1785
  // docs/filters.md
1786
1786
  var filters_default = '# Filters \u2014 choosing records by their fields\n\nOne grammar for every tool that takes a filter: `query_records`, `aggregate_records`, `update_records`\nand `delete_records` by filter, a view\'s filter, a rollup\'s filter, and the filters a caller passes\nto an app\'s query.\n\n## The shape\n\nA filter is a single condition, or a group of conditions.\n\n```\n{ "node_type": "condition", "field_key": "status", "operator": "has_any_of", "value": ["done"] }\n{ "node_type": "group", "logic": "and" | "or", "children": [ \u2026conditions or groups ] }\n```\n\nGroups nest up to 4 levels. AND(OR(status = A, status = B), age > 40):\n\n```\n{ "node_type": "group", "logic": "and", "children": [\n { "node_type": "group", "logic": "or", "children": [cond1, cond2] },\n cond3\n] }\n```\n\nEvery `field_key` resolves against the table being filtered \u2014 read each table\'s fields (`get_table`)\nand carry no name across tables: sibling tables holding the same concept routinely name it\ndifferently. A field is named by its alias (`"status"`), as `get_table` shows it, and a select option the same way\n(`"done"`). A traversal\'s `path` and the field of its condition are named the same way, each on the table the\npath has reached.\n\n## One family of operators\n\nA condition may state these operators; each reads as the type\'s own below. Which field kinds take each:\n\n| Operators | Field kinds | Meaning |\n|---|---|---|\n| `is`, `is_not` | a field holding one value: text, number, date, yes/no (`is` only, or `is_not` with a yes/no value), a select or member that is not multi, a link of cardinality one | equal to one value; on a select, member or link, to the one option, member or record |\n| `is_any_of`, `is_none_of` | text, number, date, select, member, link | equal to any of a list; on a select, member or link, `has_any_of` and `has_none_of` |\n| `lt`, `lte`, `gt`, `gte` | number, date | below, at most, above, at least |\n| `is_empty`, `is_not_empty` | every kind but yes/no | no value, or one |\n\n`is` and `is_not` on a field that holds several options, members or records (a multi select, a many-link) are refused:\nstate `has_any_of`, `has_none_of` or `has_all_of`, which say whether the row must hold exactly them or just some.\nA date is `"2026-10-15"` for that whole day, whatever time a value holds. An `is_any_of` or `is_none_of` on a number\nor a date, and an `is_not` on a date, stand for a group of conditions, which a traversal\'s single condition cannot hold;\na list that a workflow computes is expanded when the step runs, so a saved workflow states it as it states any list.\nThe depth a filter may nest is the depth you write: the groups an operator stands for do not count.\n\n```\n{ "node_type": "condition", "field_key": "due", "operator": "gte", "value": "2026-10-01" }\n{ "node_type": "condition", "field_key": "status", "operator": "is_any_of", "value": ["done", "shipped"] }\n```\n\n## Text\n\n| Operators | Value |\n|---|---|\n| `equals`, `not_equals`, `contains`, `does_not_contain`, `starts_with`, `ends_with` | a string |\n| `is_any_of`, `is_none_of` | a string array \u2014 exact, case-insensitive |\n| `contains_any_of` | a string array \u2014 the cell contains any of them, case- and accent-insensitive |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "title", "operator": "is_any_of", "value": ["hello", "world"] }\n```\n\nA text value is a string, never an array; `is_any_of` matches several. Its array has no length cap \u2014\npass the whole list in one condition rather than splitting it across calls.\n\n## Number\n\n| Operators | Value |\n|---|---|\n| `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to` | a number |\n| `is_empty`, `is_not_empty` | none |\n\nA number whose `notation` reads its unit or currency on each row (`{ "per_row": <select> }`) is\nread in each row\'s own, so a comparison on it states `unit_option`: the option of that select its\nvalue is in.\nRows holding another option are out; measured units of one dimension convert.\n\n```\n{ "node_type": "condition", "field_key": "amount", "operator": "greater_than", "value": 100, "unit_option": "USD" }\n```\n\n## Boolean\n\n`equals` with `true` or `false`.\n\n## Select\n\n| Operators | Value |\n|---|---|\n| `has_any_of` (any of them \u2014 "A or B"), `has_none_of`, `has_all_of` (a multi-select only) | an ARRAY of option aliases, even for one |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "status", "operator": "has_any_of", "value": ["done", "in_progress"] }\n```\n\n## Member\n\n| Operators | Value |\n|---|---|\n| `has_any_of`, `has_none_of`, `has_all_of` | an array of member ids |\n| `is_current_member`, `is_not_current_member`, `is_empty`, `is_not_empty` | none |\n\n## Date\n\n| Operators | Value |\n|---|---|\n| `before`, `after`, `on_or_before`, `on_or_after`, `on`, `starts_before`, `starts_after`, `ends_before`, `ends_after` | a point |\n| `between`, `overlaps`, `contains`, `within` | `{ start: point \\| null, end: point \\| null }` |\n| `time_of_day` | `{ start_time: "HH:mm" \\| null, end_time: "HH:mm" \\| null }` |\n| `duration_equals`, `duration_greater_than`, `duration_less_than` | `{ amount, unit: "minutes" \\| "hours" \\| "days" }` |\n| `is_empty`, `is_not_empty` | none |\n\nA date value is a point. A bare `"YYYY-MM-DD"` is the point of that whole day (`"YYYY-MM-DDTHH:mm"` of that minute),\nand a stated date is compared as written, with no timezone shift: `on "2026-10-15"` matches every value from 00:00 to\n23:59 of that day, on a date or a datetime field. `before` the day is earlier than its start, `after` later than its end,\n`on_or_before` and `on_or_after` include it. A relative or period point counts today in the field\'s timezone. The\nobjects below say the same, and add a time, an offset or a period:\n\n```\n{ "type": "exact", "date": "2025-03-14", "time": "09:00" } time optional\n{ "type": "relative", "offset": -7, "unit": "minutes" | "hours" | "days" | "weeks" | "months" | "years" }\n{ "type": "period", "period": "day" | "week" | "month" | "quarter" | "year", "boundary": "start" | "end", "offset": 0 }\n```\n\n## Link\n\n| Operators | Value |\n|---|---|\n| `has_any_of` (linked to any \u2014 filters by record identity), `has_none_of`, `has_all_of` (a multi-link only) | an array of record ids (`rec_\u2026`) |\n| `contains`, `not_contains`, `starts_with`, `ends_with` \u2014 deprecated: the text a link stores changes with every rename; filter by record, and by a linked record\'s field through a lookup of it | a string, matched against the linked record\'s display text |\n| `is_empty`, `is_not_empty` | none |\n\n```\n{ "node_type": "condition", "field_key": "customer", "operator": "has_any_of", "value": ["rec_abc123"] }\n```\n\n## Through a link\n\nA traversal chooses records by a field of the records they link to. `path` lists link fields, each of the table\nbefore it (up to 3), and `condition` names a field of the last table reached:\n\n```\n{ "node_type": "traversal", "path": ["customer"], "condition": { "field_key": "tier", "operator": "has_any_of", "value": ["gold"] } }\n```\n\n## Sort\n\nA view\'s sort is a list, applied in order: `[{ "field_key": "due", "order": "asc" | "desc" }]`.\n';
1787
1787
 
1788
1788
  // docs/field_values.md
1789
- var field_values_default = '# Field values \u2014 what a write takes for each field type\n\nThe value `create_records` and `update_records` take for a field. A field is named by its alias, as `get_table` shows it, and a select option the same way. `dry_run: true` on `create_records`, `update_records` and `delete_records` runs the write in a transaction that is rolled back: every check, rule and stage owner is judged, no number is consumed, and the answer says what would be written or why it is refused. Every write meets the table\'s rules (`model/write-rules`): a refusal lists every broken rule at once, and a locked record answers under `blocked` in `update_records`\' result.\n\n## Values by type\n\n| Type | Value |\n|---|---|\n| `text` | `"hello"` |\n| `number` | `42` |\n| `boolean` | `true` |\n| `date` | `"2025-03-14"`, or a month `"2025-03"` or a year `"2025"`, stored as written and compared as its first day |\n| `datetime` | a `date` of this format: `"2025-03-14T09:00"`, never a month or a year |\n| `date_range` | a `date` of this format: `"2025-03-01/2025-03-15"`, both halves full dates |\n| `datetime_range` | a `date` of this format: `"2025-03-01T09:00/2025-03-15T17:00"`, both halves full |\n| `select` | `["open"]`, the option\'s alias |\n| `member` | `["mbr_\u2026"]` |\n| `link` | `["rec_\u2026"]` |\n| `files` | `["fil_\u2026"]`, ids of uploaded files |\n| `formula`, `rollup`, `lookup`, `autonumber`, a `date` with `derive_from` | none \u2014 the platform writes them, and a write naming one is refused |\n\n## Lists\n\n`select`, `member`, `link` and `files` store an ARRAY, even where the field holds\none value.\n\n- A single-select is a ONE-element array; a bare `"open"` is accepted and wrapped. A single\n `member` takes a bare `"mbr_\u2026"` the same way.\n- A single `link` takes a bare `"rec_\u2026"` the same way; a multi `link` and `files` take an array only.\n- A `files` id the write adds must name a live file of this workspace, or the whole write is refused;\n an id that cell already holds stays, though its file was archived since.\n- A file previews by its kind: an image zooms; a PDF scrolls, zooms sharp and turns by page, its text selectable; a DXF\n drawing shows read-only in 2D with a toggle per layer. A DWG stays a stored file with no preview; attach its plotted\n PDF beside it.\n- Two options on a single-select, two members on a single `member`, or two records on a single `link`, are refused.\n- `update_records`\' `add_to`, `remove_from` and `replace` take arrays of the same items: option aliases\n for a select, member ids for a `member`, record ids for a `link`, file ids for\n `files`.\n';
1789
+ var field_values_default = '# Field values \u2014 what a write takes for each field type\n\nThe value `create_records` and `update_records` take for a field. A field is named by its alias, as `get_table` shows it, and a select option the same way. `dry_run: true` on `create_records`, `update_records` and `delete_records` runs the write in a transaction that is rolled back: every check, rule and stage owner is judged, no number is consumed, and the answer says what would be written or why it is refused. Every write meets the table\'s rules (`reference/write-rules`): a refusal lists every broken rule at once, and a locked record answers under `blocked` in `update_records`\' result.\n\n## Values by type\n\n| Type | Value |\n|---|---|\n| `text` | `"hello"` |\n| `number` | `42` |\n| `boolean` | `true` |\n| `date` | `"2025-03-14"`, or a month `"2025-03"` or a year `"2025"`, stored as written and compared as its first day |\n| `datetime` | a `date` of this format: `"2025-03-14T09:00"`, never a month or a year |\n| `date_range` | a `date` of this format: `"2025-03-01/2025-03-15"`, both halves full dates |\n| `datetime_range` | a `date` of this format: `"2025-03-01T09:00/2025-03-15T17:00"`, both halves full |\n| `select` | `["open"]`, the option\'s alias |\n| `member` | `["mbr_\u2026"]` |\n| `link` | `["rec_\u2026"]` |\n| `files` | `["fil_\u2026"]`, ids of uploaded files |\n| `formula`, `rollup`, `lookup`, `autonumber`, a `date` with `derive_from` | none \u2014 the platform writes them, and a write naming one is refused |\n\n## Lists\n\n`select`, `member`, `link` and `files` store an ARRAY, even where the field holds\none value.\n\n- A single-select is a ONE-element array; a bare `"open"` is accepted and wrapped. A single\n `member` takes a bare `"mbr_\u2026"` the same way.\n- A single `link` takes a bare `"rec_\u2026"` the same way; a multi `link` and `files` take an array only.\n- A `files` id the write adds must name a live file of this workspace, or the whole write is refused;\n an id that cell already holds stays, though its file was archived since.\n- A file previews by its kind: an image zooms; a PDF scrolls, zooms sharp and turns by page, its text selectable; a DXF\n drawing shows read-only in 2D with a toggle per layer. A DWG stays a stored file with no preview; attach its plotted\n PDF beside it.\n- Two options on a single-select, two members on a single `member`, or two records on a single `link`, are refused.\n- `update_records`\' `add_to`, `remove_from` and `replace` take arrays of the same items: option aliases\n for a select, member ids for a `member`, record ids for a `link`, file ids for\n `files`.\n';
1790
1790
 
1791
1791
  // docs/workflows.md
1792
- var workflows_default = '# Workflows \u2014 the steps a workflow runs\n\nA workflow is written as a strict subset of JavaScript. The source is never run as JavaScript: a\nsave parses it into steps, type-checks it against what its trigger supplies, and stores the steps.\nOnly the forms below are accepted; anything else is refused at save with the line, the column and\nwhat to write instead. A save that succeeds may still return `warnings` \u2014 advisory hints such as a\nloop that may never end. Read them.\n\nA workflow changes records, calls integrations, notifies and makes papers. A tool for anything else \u2014 views, history,\ntables and apps, members, knowledge, skills, editing a file \u2014 is an agent\'s, on chat, MCP or the CLI, and a body that\ncalls one is refused at save with that reason. One saved before is refused at run, before its first step.\n\nThe same grammar serves an app\'s workflow, which `set_app_workflow` saves, an automation and the expressions of a table\ncheck. An app workflow takes its trigger from the app that calls it; only an automation\'s source opens with a trigger\ndeclaration.\n\n## Triggers\n\nAn app workflow reads its call as `trigger.app_workflow.inputs.<name>`, or `inputs.<name>`, which is the same read. A Workflow node receives the inputs it declares;\nan Act\'s `workflow` receives `record_id` (`record_ids` on an act over several rows) and what the Act asks.\n\n### Automations\n\nAn automation answers an event outside the records: a schedule, a webhook, an inbound email. `set_automation` saves one\nfrom its source \u2014 the `on({...})` declaration, then its steps \u2014 and, given its `automation_id`, replaces it: the\ntrigger config is the one the header states, a key it leaves out removed, and steps left as it runs them are checked again\nonly when the call turns it on, so one a later rule refuses can still be switched off. `get_automation` reads that source back with its latest\nruns, and `dry_run_workflow` rehearses a source before it is saved. It runs with its owner\'s current permissions, a connected service it names is its owner\'s account, and only its\nowner or an admin reads, changes or deletes it.\n\n```\non({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });\n```\n\nEvery source below is typed, so a misspelled member is refused at save rather than read as nothing.\nAll three carry `trigger.trigger_id`.\n\n| trigger | required config | what the body reads |\n|---|---|---|\n| `recurring_schedule` | `cron_expression` | `runtime.timezone`; the current time is `now()` |\n| `receive_webhook` | `secret`? | `trigger.method`, `trigger.headers[...]`, `trigger.query[...]`, `trigger.body` (any JSON, or raw text when the request is not JSON), `trigger.received_at` |\n| `receive_gmail_email` / `receive_outlook_email` | `connected_account_id`, `filter` | `trigger.email_id`, `trigger.from`, `trigger.to`, `trigger.cc`, `trigger.bcc`, `trigger.subject`, `trigger.date`, `trigger.body` (plain text), `trigger.reply_to`, `trigger.in_reply_to`, `trigger.attachments` (file refs \u2014 assign them straight to a files field). One type serves both providers, so `labels`, `thread_id`, `importance` and `conversation_id` are not readable. |\n\n## Steps\n\n| step | syntax |\n|---|---|\n| tool call, kept | `const <id> = await <tool>({ ...inputs });` \u2014 also `let x = await \u2026` and `x = await \u2026` |\n| tool call | `await <tool>({ ...inputs });` \u2014 the platform\'s own steps (`send_notification`, `generate_document`, \u2026) and a connection\'s tools, each a workflow step (`bkav_submit_invoices`, `kiotviet_create_order`, \u2026); a step that fails ends the run with its reason unless a `try`/`catch` around it handles it |\n| agent | `const <id> = await agent({ instructions, input, model, output });` \u2014 see **An agent step** |\n| app agent | `const <id> = await app_agent({ alias: "<agent alias>", input: { ... } });` \u2014 runs one of the app\'s declared agents, in an app workflow only, and resolves to its declared outputs or its final text. `wait: false` only starts the run and resolves to `{ run_id }`; the run\'s failure is then its own, not the workflow\'s. Refused in a run an agent started. |\n| bind | `const <id> = <expression>;` \u2014 evaluated once; later steps read `<id>`. Bind any expression used twice. |\n| if / else | `if (<expr>) { ... } else { ... }` \u2014 `else` optional, `else if` chains allowed |\n| switch | `switch (<expr>) { case "X": { ... } default: { ... } }` \u2014 string cases only, no fallthrough |\n| for | `for (const <name> of <expr>) { ... }` |\n| wait | `await wait({ duration_in_minutes: 5 });` |\n| wait for an event | `await wait_for_event({ event_type: "webhook", event_ref: <expr>, timeout_in_minutes: 60 });` |\n| wait for an approval | `await wait_for_approval({ approvers: <expr>, prompt: <expr> });` \u2014 see **Waiting for an approval** |\n| return | `return({ status: "success" \\| "error", message: <expr>, field_errors: { ... }?, data: { ... }? });` \u2014 `data` holds the outputs a Workflow node declares |\n| validate | `validate({ checks: [{ fail_when: <expr>, field_key: "...", message: <expr> }, ...] });` |\n\nA delayed run is never cancelled from a workflow: after a `wait`, it reads the row\'s state and acts only if it still holds.\n\nA tool\'s inputs and answer are in its own description; the steps only a body runs \u2014 a connection\'s, a mail\'s \u2014 are each\nan entry of `workflow_steps`. A person connects a service in Lotics under Settings \u2192 Connections; a step names that\nconnection by its `connected_account_id`, which `get_workspace` with `kind: "connection"` lists for the connections the\ncaller can use. The app\'s owner and whoever saves the body must both be able to use that connection.\n\nIn a workflow, email reaches a member of the workspace through the step `send_notification`: Lotics shows it to them and\nmails it to their own address, with no connection. Its recipients are members by `member_id` (`get_workspace` with\n`kind: "member"` lists them), member groups or the whole organization. Mail to anyone else goes from a person\'s\nconnected Gmail or Outlook (`gmail_send_email`, `outlook_send_email`), which leaves an unsent draft unless `draft` is\n`false`.\n\n`return` is a call, not a JavaScript `return` statement. `validate` ends the workflow with an error\nwhen a check\'s `fail_when` is truthy; the check\'s `field_key` names the control its message lands on \u2014\nin an app workflow a declared input, or the alias of a field on a table the body names; in an\nautomation it is not checked.\n\nThe name in `const x = await tool({...})` is the step\'s id; later expressions read its output as\n`x.records`, `x.id` and so on. A `// id: my_step` comment directly above a statement sets an explicit\nid; any other `//` comment there becomes the step\'s description.\n\n**`await` inside a statement** runs the call as its own step first, then reads its output where it is\nwritten: `size((await query_records({...})).records)`, an `if` test, a `return` value, a `for-of`\niterable, a tool input. `const x = c ? await t({...}) : null;` is stored as the `if` it means. An\n`await` that would run on some paths only \u2014 inside `&&`, `||`, `??`, `?.`, a nested ternary, a loop\ntest, a `validate` check \u2014 is refused; write the `if`.\n\n**Names are block-scoped, as in JavaScript** \u2014 `const`, `let`, loop items and `catch (e)`: sibling\nblocks may reuse a name and an inner block may shadow an outer one. A local may take a helper\'s name\n(`const size = 3;`), but calling `size(...)` while it is in scope is refused. The names of the tools a\nworkflow calls, the reserved roots, `linked` and `_s` followed by digits cannot be bound.\n\n### Waiting for an approval\n\n`wait_for_approval` is the wait worth binding \u2014 `wait` and `wait_for_event` carry nothing to read.\nBind it to branch on the decision:\n\n```\nconst approval = await wait_for_approval({\n approvers: trigger.app_workflow.inputs.approvers,\n prompt: `Approve this quote?`,\n});\n\nif (approval.status == "approved") {\n // the approved path\n} else {\n // the rejected or timed-out path; approval.decision_comment says why\n}\n```\n\nIt resolves to `{ status: "approved" | "rejected" | "timed_out", decided_by: MemberId | null,\ndecided_at: ISO string, decision_comment: string | null }`.\n\n`approvers` takes a bare member id (`"mbr_\u2026"`), a bare group id (`"grp_\u2026"`) or a full principal\n(`{ type: "member_group", id: "grp_x" }`), alone or in a list \u2014 so a member field\'s value passes as it\nis.\n\n### An agent step\n\n`const x = await agent({ instructions, input, model, output });` runs a model as one step \u2014 read a paper, classify,\ndraft text. It calls no tool: the workflow\'s own steps write, send and notify with what it returns, and a step that\nstates `tools` is refused.\n\n- `instructions` \u2014 a fixed string.\n- `input` \u2014 an object literal; its fields are expressions, evaluated and handed to the model (`{}` for\n none). It is the ONLY workflow data the agent sees: read the trigger, prior steps, `runtime` or the loop\n item here, never inside `instructions`, which is a fixed literal and reads no binding. A field holding a file, or a\n list of files (a files field, a mail\'s attachments), hands the agent the files themselves: a picture or a PDF it\n sees, any other paper read into text.\n- `model` \u2014 optional; omit it to follow the platform\'s default model.\n- `output` \u2014 `{ mode: "text" }` resolves to a string; `{ mode: "object", schema }` to a typed object.\n\nOnly `x` comes back: in text mode `x` is the string itself, in object mode `x.field` reads a field the\n`schema` declares.\n\n## Naming tables and templates\n\nA body names a table or document template by its alias (`query_records({ table_id: "orders" })`); ids still work. `get_app_workflow` and `get_automation` show aliases. An app\'s workflow reaches only the tables the app uses.\n\n## Fields and options\n\n1. **A body names a table\'s fields and select options by alias**, the names the table\'s document states: a field reads\n as `record["status"]`, an option as `"done"`, a linked field as `linked(record["customer"])[0]["name"]`, in the\n body of a Workflow node, of an Act\'s own `workflow`, of an automation (`set_automation`) and of a table\'s workflow. It is stored by key and `get_app_workflow`, `get_automation` and `get_table` show it by alias\n again. A field\'s or option\'s key is accepted where its alias is and is kept as written; a display name is refused with the\n aliases there are, and a row that names one field by both its alias and its key is refused. A row written to a table is\n stored by field key wherever the save can follow it to the record tool that writes it: a row or list written in the call,\n a conditional of them, a name holding them, pushed to a `let`, and through `map`, `flatMap`, `concat`, `filter`, `sortBy`,\n `slice` and `reverse`. A field of such rows is read by alias as well (`picks.toSorted((a, b) => a.quantity - b.quantity)`,\n `sortBy(picks, "quantity")`, `rows[0].note`) and stored by key. A body that cannot be stored by key is refused at save,\n naming the statement.\n2. **An Act\'s own `workflow`** has the app\'s guards placed at the top of the body, between `<lotics:guards>` tags,\n replaced on every save: a save that leaves them out or edits them gets the app\'s again, at the top wherever the tags\n sat, so the act\'s row rule, `when`, `requires`, checks and locks always run first.\n3. **A value is read as the record tools store it.** A single select reads as its option and a member as its id; an\n option is written by its alias wherever it meets a select \u2014 compared, listed, switched on, written \u2014 also beside a\n name set from the field (`const s = record.status; s == "done"`), and a select\n value handed on whole (returned, written to a text field) is its key. An option\'s alias searched for in a list the\n save cannot place it against (`includes(filter(record.tags, \u2026), "hot")`) is refused: search the field itself. A link reads as the list of its row ids, a\n one-row link too: `first(record.customer)` is the one id, and `record.customer` used as one id is refused at save,\n naming `first(record.customer)`. A one-row link is written with its one id or a list of one; a many-row link with a list. A lookup reads as one flat list of the values it looks up: `includes(record.owner,\n me)`. A select\'s value is one of another select\'s options only where that table shares its keys, by having been\n duplicated from it, not where an option merely shares its alias. A row that takes its parent\'s option gets it from\n `default_from`, and a lookup shows it.\n4. **What an app sends takes an option by alias or key.** An input or output drawn from a field (`field`, written\n `<table id>.<alias>`) compares and returns by alias in the body, and the run hands it on as the option\'s key. A\n refusal\'s `field_errors` and a `validate` check\'s `field_key` name a declared input or a field\'s alias.\n5. **`==` against a multi-select means "includes".** `record["tags"] == "urgent"` on a multi-select is stored as\n `includes(record["tags"], "urgent")`; either spelling works.\n6. **Text renders labels.** Inside a template or a text tool input, a select, member or link value renders its display\n text: `` `${record["status"]}` `` writes "Done", not `done`.\n\n## What an expression reads\n\n- `record`, `prev_record`, `changes` \u2014 in a table check, the write it checks (see **Table checks**).\n `changes` is a PARTIAL map: `changes["x"]?.next_value` and `?.prev_value`, the `?.` required.\n- `trigger` \u2014 what the trigger carries (see **Triggers**).\n- a loop\'s item name, and `index`, the iteration\'s 0-based index.\n- `<step id>` \u2014 an earlier step\'s output (`dup.records`).\n- `runtime.timezone`, `runtime.workflow_id`, `runtime.execution_id`, `runtime.workspace_id`,\n `runtime.organization_id`, `runtime.change_origin`, `runtime.triggered_by_member_id`. The current\n time is `now()`.\n\n## Paths\n\n- `record.x` and `record["x"]` read a field; `[n]` reads a list\'s n-th item.\n- A path may start at any expression: an awaited call `(await get_record({...})).data["x"]`, a helper\'s\n result `first(found.records).id`, a filter `rows.filter((r) => r.ok)[0].n`, a `(a ?? b).c`. `?.` reads\n through a null as usual, except on a value drawn at random (`sample(xs)?.id` is refused: bind it first).\n- A record reads in its declared shape wherever it comes from \u2014 the trigger, a tool result, a `let`, a\n callback parameter: a single select as its option alias or null, a member as its id, a link as its ids. (The record\n tools called outside a workflow return the stored arrays instead.)\n- `linked(record["link"])[n]["x"]` fetches the n-th linked row and reads a field of it; bare\n `linked(record["link"])` is every linked row, one fetch per row. Its argument is a path ending on\n a link field and carries the guard: `linked(record?.["link"])[0]`.\n- `changes["link"]?.next_value` is the same list of ids; `linked()` over a change is refused \u2014\n read `linked(record["link"])` or `linked(prev_record["link"])`.\n\n## Operators and statements\n\nOperators in JavaScript precedence: `? :`, `||`, `??`, `&&`, `== != === !==`, `< <= > >=`, `+ -`, `* / %`,\nprefix `! -`.\n\n- Arithmetic is exact decimal (`0.1 + 0.2` is `0.3`). A result computed from a division that does not end\n is the short decimal it reads as (`1 / 3 * 3` is `1`), and stands as computed where it reads long\n (`1 / 3 * 100` is `33.33333333333333`). A comparison between two numbers reads both at 15 significant\n digits. `round` and `formatNumber` round the decimal, a half away from zero: `round(-2.5)` is `-3`. A\n table filter compares stored values exactly.\n- `??` is `coalesce(left, right)`; `?.` reads through a null (`a?.b`, `a?.[0]`, and `s?.trim()` for the\n method names listed under **Helpers**). `x?.()` is refused: helpers and tools are not values.\n- Templates use backticks and `${...}`.\n- Destructuring \u2014 `const { status, name } = record;` binds each name. Renames\n (`{ status: s }`), quoted keys (`{ "ref": ref }`), array patterns with holes\n (`const [first, , third] = xs;`) and defaults (`{ note = "" }`, which apply on null too) work on\n `const`, `let` and `for (const { id } of rows)`. A tool result destructures directly:\n `const { records } = await query_records({...});`. `const a = 1, b = 2;` declares both. Nested\n patterns and rest are refused.\n- Spread \u2014 `[...a, b]` and `{ ...a, b: 1 }` (later keys win). Refused inside a tool input: bind the\n merged value first and pass the binding.\n- Shorthand \u2014 `{ table_id }` is `{ table_id: table_id }`.\n- `xs.push(a, b);` appends, also on a key (`o.items.push(v)`); `o.a.b = v;` sets a nested key;\n `x ??= v`, `x ||= v` and `x &&= v` assign. A `const` may be pushed to or have a key set, as in\n JavaScript; a loop item is read-only.\n- `undefined` is the same value as `null`, so `x === undefined` is `isNull(x)` and `x !== undefined` is\n `isNotNull(x)`; a key set to null and a key never sent are not told apart (`"k" in o` tells a key that\n is there).\n- `typeof x` answers JavaScript\'s words, and a null (a missing value too) is `"object"`. `k in o` tests an\n object\'s own keys, or a list\'s indexes and `length`.\n- A filter node may leave out `node_type` when its keys say which it is: `field_key` / `operator` /\n `value` a condition, `logic` / `children` a group, `path` / `condition` a traversal.\n- `function name(p = <default>) { return <expr>; }` \u2014 top level only, expanded at every call (a call\n may come before it). Every parameter needs a default, which gives it its type; the body is one\n `return <expr>;` reading only its parameters, helpers and the reserved roots \u2014 never the caller\'s\n names, nor `index`. No recursion; a function never called is refused. A body read back shows the\n expression at each call, not the `function`.\n\n**try / catch.** `try { ... } catch (e) { ... }` catches a tool error or an expression error inside the\nbody; `e` is `{ message, type, step_id?, detail? }`. A failed `validate` and a `return` are not\nerrors \u2014 they end the workflow \u2014 and a step that runs after a wait inside the `try` is outside it.\n`finally` is refused: put always-run steps after the `try`.\n\n**Loops.** `for-of`, `while (cond) { ... }`, `do { ... } while (cond);` and\n`for (let i = 0; i < n; i++) { ... }`, each capped at 10,000 iterations. `break;` and `continue;` act\non the innermost loop; labels and `for-in` are refused.\n\n**`let`.** `let x = <expr>;` declares a block-scoped variable; the initializer is required\n(`let x = null;`). Reassign with `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `??=`, `++` and `--`. Re-declaring in\nthe same block is refused; shadowing in a nested block is allowed. A `let` keeps its value across a\n`wait`, `wait_for_event` or `wait_for_approval`.\n\n**Refused, each because a save must know what runs and a run must give the same answer twice:** regex\n(unbounded matching: use the string methods), `Math.random`, `Date.now()` and `new Date()` (use `now()` and\n`parseDate`), a tool or helper as a value or a computed callee (callees are named), `new` of anything,\n`instanceof`, `delete`, `void`, rest elements, computed keys, classes, `throw`, `finally`, `import`,\n`export`, `eval`, labels, and a function as a value (`const f = (x) => ...`: pass the arrow to the helper\nthat takes it). `xs.sort(...)` on a name or path is refused too, since a value is never changed in place:\nwrite `xs.toSorted(...)` or `[...xs].sort(...)`.\n\n## Typing\n\n- The body is type-checked as TypeScript before it is read: annotations, `as`, `satisfies` and a non-null `!`\n are accepted and erased, and the checker is strict about null. The stored body keeps none of them and is checked again as\n stored, by field key, as every later change to its app or tables checks it: a body that types only through one of them is\n refused at save.\n- Narrow what is not typed (a connector\'s JSON is `unknown`) with `typeof`, `in`, `Array.isArray` and `isNull`.\n- `first(xs)` may be undefined; `requireFirst(xs)` is the list\'s first item or an error.\n- `obj[key]` needs `key` to be a key of `obj`, or `obj` to be a record with a string index.\n\n## Helpers\n\nOrdinary JavaScript works \u2014 the standard library below, the array and string methods, `Math`, `JSON`,\n`Object`, `Array`, `Number`, `parseInt` \u2014 and means what it means in JavaScript, with the differences\nlisted here. Where JavaScript answers NaN, Infinity or undefined, a workflow has none of them and the\nanswer is `null`.\n\n- Standard library: `Math.abs/ceil/floor/trunc/sign/min/max/pow/sqrt`, `Number(x)`, `String(x)`,\n `Boolean(x)`, `parseInt(s, radix?)`, `parseFloat(s)`, `Number.isFinite/isInteger`,\n `Object.keys/values/entries/fromEntries`, `Array.isArray/from` (a list, text, or `{ length: n }`),\n `JSON.parse/stringify`.\n- Arrays: `length, map, filter, find, findIndex, findLast, some, every, reduce, includes, indexOf, join,\n slice, concat, flat, flatMap, at, reverse, toReversed, toSorted` and `[...xs].sort(compare?)`.\n- Strings: `length, toLowerCase, toUpperCase, trim, trimStart, trimEnd, startsWith, endsWith, includes,\n indexOf, slice, substring, split, replace, replaceAll, padStart, padEnd, repeat, charAt, at,\n localeCompare` (en-US order, wherever it runs). Numbers: `toFixed, toString`.\n- Differences from JavaScript: `Math.round` is the engine\'s `round` (a half goes away from zero:\n `Math.round(-2.5)` is `-3`); `Number(x)` and `parseFloat(x)` are `toNumber`, which refuses text that is no\n number; `String(null)` is `""`; `join` writes a null item as `null`; `concat` skips a null; `toFixed`\n rounds the decimal, not the binary double; `replaceAll` writes the replacement as text\n (`$&` stays as typed), while `replace` expands `$&`, `$$`, `` $` `` and `$\'` in it as JavaScript does; `split` takes no limit; `sort` without a comparator orders by text, as JavaScript does. `==`, `!=`, `===` and `!==` between two list values (files, a multi-select, a link of any cardinality, a lookup) are refused at save, since a list compares by identity, not by its items: `!isNull(changes.<field>)` asks whether one changed.\n\nThe same work can be written with the helpers below, which are additions to JavaScript. They are called as\n`size(arr)`; the method form works only where the name is also a JavaScript method \u2014\n`x.trim()`, `arr.includes(v)`, `arr.at(-1)`, `arr.map(fn)`, `s.split(",")`; `arr.size()` is refused.\n\n- **Null and type**: `isNull`, `isNotNull`, `isEmpty`, `isString`, `isNumber`, `isBoolean`, `isArray`,\n `isObject`, `coalesce`, `get`, `toNumber`, `toFigure`, `toString` (a text, number, yes/no or date; a record, file or list is refused at save, an object at run), `typeOf`, `parseJson`, `toJson`. `isNull` is strict (`null` and `undefined` only), but on a list field of the record (a link, files, a multi-select) it asks whether the list holds nothing, since a cleared list is `[]`: `!isNull(record.vehicle)` reads as `!isEmpty(record.vehicle)`. `isNull(changes.<field>)` stays strict: null there means unchanged\n- **Lists**: `size`, `first`, `requireFirst` (the first item, refusing an empty list \u2014 after a\n `validate` on the size it saves an `if`), `last`, `nth`, `at` (`at(arr, -1)` counts from the end),\n `includes`, `filter`, `find`, `some`, `every`, `pluck`, `sortBy`, `groupBy`, `countBy`, `unique`,\n `uniqueBy`, `compact` (drops falsy items), `flatten`, `reverse`, `slice`, `concat`, `difference`,\n `differenceBy`, `intersection`, `intersectionBy`, `list`, `reduce(arr, (acc, x) => ..., initial)`,\n `range(end)` / `range(start, end)`\n- **Math**: `sum`, `sumBy`, `mean`, `meanBy`, `minBy`, `maxBy`, `round`, `ceil`, `floor`, `min`, `max`,\n `abs`, `mod`, `pow`, `sqrt`, `clamp`, `percentage`\n- **Text**: `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`,\n `replaceAll`, `removeChars(str, chars)` (without every character that occurs in `chars`),\n `removeSpaces(str)` (without any whitespace; a Word template command calls both too), `substring`, `length`, `split`, `join`, `padStart(str, length, char)`,\n `padEnd(str, length, char)`, `formatNumber(x, decimals)` (ungrouped; rounds the decimal, a half away\n from zero: `formatNumber(1.005, 2)` is `1.01`),\n `formatDecimal(x, decimals, locale)` (grouped: `formatDecimal(151000, 0, "vi-VN")` is `151.000`),\n `numberToWords(x, lang?)` (an integer in words: Vietnamese, or English with `"en"`),\n `uuidFrom(text)` (the same UUID for the same text, refusing null and empty text: `uuidFrom(record.id)`)\n- **Objects**: `keys`, `values`, `entries`, `nonNullKeys`, `pick`, `omit`, `merge`\n- **Dates**, in the workspace\'s timezone: `now`, `formatDate`, `parseDate`, `addDays`, `subDays`,\n `addHours`, `subHours`, `addMinutes`, `subMinutes`, `startOfDay`, `endOfDay`,\n `differenceInCalendarDays`, `differenceInHours`, `differenceInMinutes`, `isBefore`, `isAfter`,\n `isSameDay`, `isToday`, `isWithinRange`, `meetsBounds`\n- **Other**: `formatCurrency(amount, locale, currency)`, `randomNumber(len)`,\n `randomAlphaNumeric(len)`, `sample(items)` (a random item)\n\nOver a null or empty list `mean`, `meanBy`, `min`, `max`, `minBy` and `maxBy` answer null, while `sum`\nand `sumBy` answer 0: `maxBy(pieces, "reach")?.Name` prints a blank for a month with no pieces.\n\nA date helper given a null or empty date returns null, so guard its result before comparing or\nwriting it: `differenceInCalendarDays(a, b) ?? 0`.\n\n`filter`, `find`, `some`, `every`, `sortBy`, `pluck`, `sumBy`, `meanBy`, `minBy`, `maxBy`, `groupBy`,\n`countBy`, `uniqueBy`, `differenceBy` and `intersectionBy` take a path string\n(`filter(record.items, "Status", "open")`) or a callback\n(`filter(record.items, x => x.Status == "open" && x.Amount > 100)`); `reduce` takes a callback and an\ninitial value. A callback gets `(item, idx)` (`reduce`: `(acc, item, idx)`) and reads `record`,\n`runtime`, the names in scope, loop items and an enclosing callback\'s parameters. Its body is one\nexpression \u2014 `(x) => <expr>`, `(x) => { return <expr>; }` or `function (x) { return <expr>; }`;\nseveral statements, `async` and named function expressions are refused.\n\nLook a row up by key rather than filtering for it: a `filter` inside a callback walks its whole list again\nfor every item, so a thousand items against a few hundred rows is hundreds of thousands of calls. Group the\nrows once, then read each group by its key. `groupBy` answers an object of lists, and `byCode[code] ?? []`\nis the group a code names, or an empty list:\n\n```\nconst parties = await query_records({ table_id: "parties" });\nconst lines = await query_records({ table_id: "sheet_lines" });\nconst byCode = groupBy(parties.records, (p) => p.data["code"] ?? "");\nconst holders = lines.records.map((line) => ({\n line: line.id,\n parties: (byCode[line.data["code"] ?? ""] ?? []).map((p) => p.id),\n}));\n```\n\nA key is read exactly: a code holding a dot, a slash or a bracket reads its own group, and a name every\nobject inherits (`constructor`) reads nothing unless a row made a group of that name. `get(o, "a.b")`\nfollows a dotted path instead, so a key that holds a dot is read with `o[key]`.\n\n## Writing a field: null and undefined\n\nIn `update_records`\' `set` and `create_records`\' `records`:\n\n- `null` clears the field.\n- `undefined`, or leaving the key out, leaves the field as it is.\n\nSo passing a read that may be null (`record["x"]`) keeps a value when there is one and clears the\nfield when there is none. Use `coalesce(x, fallback)` only for a real fallback value.\n\n## Examples\n\nCompare a link by id. Display text is not unique, and a link reads as a list of ids \u2014 `==` against a\nstring is a type error:\n\n```\nconst found = await query_records({\n table_id: "customers",\n filters: { node_type: "condition", field_key: "name", operator: "equals", value: "ACME Corp" },\n});\nconst customer = first(found.records);\nconst order = await get_record({ table_id: "orders", record_id: trigger.app_workflow.inputs.order_id });\n\nif (customer && includes(order.data["customer"], customer.id)) {\n // \u2026\n}\n```\n\nRead an uploaded sheet and add the rows a table lacks. `excel_get_rows`, a workflow step, answers every\nrow of an `.xlsx`, `.xls` or `.csv` sheet in one call, each an object keyed by the header row\'s text. A cell is a number,\n`true` or `false`, text, a date as ISO text, or null when blank, so `row["Header"]` is typed\n`string | number | boolean | null` and a helper narrows it: `toString` for text, `toNumber` for a number,\n`isString` before a date. Every cell of a `.csv` is text. `columns` keeps only the ones the body reads.\nA sheet past what one call reads is refused with its size, never cut short: the step\'s entry in\n`workflow_steps` states the bounds.\n\n`import_rows`, a workflow step, matches those rows to a table by a key and writes what differs. `match`\nnames the fields that identify a row, compared as trimmed text with case ignored: a row matching no live\nrow is added, one matching a row changes only the cells that differ, and one that changes nothing is\ncounted and not written, so the same file run twice writes nothing the second time. `values` holds a\nrow\'s cells by field alias, as `create_records` takes them; a key the row omits is left as the table holds\nit, and `null` is an empty cell, which differs from a filled one as any other value does. `dry_run: true` judges the import as a real one would be, rolls it back and answers what it would write or why it is refused.\n\nA `match` field may also be a link field holding one row, compared by the row linked: lines keyed by\ntheir parent and a line number state `match: ["invoice", "line"]`, the parent given as its\nrecord id or as `{ lookup: "<text>" }`.\n\nWhat the step is not certain of is held \u2014 answered with why, and not written: a row carrying `hold`\n(its text is the message); a row whose key several live rows carry, or an earlier row of the same call;\na row the table\'s checks refuse, or whose row is locked; with `create: false`, a row matching none. A\nheld row never fails the step, and the rows beside it are written. A row whose key names a row outside\nthe rows the run may write is held `refused` with nothing of that row: no `record_id`, no `changes`.\n\nThe step removes no row, and a row the source leaves out is never touched. A row carrying\n`remove: true`, with its key in `values`, proposes moving the row its key names to the trash: it is\nalways held, whatever `create` and `conflicts` say, and a key naming no row is counted unchanged.\n\n`conflicts` states what a matched row does where a live cell holds another value: `"hold"` writes the\nrow\'s empty cells and holds the differing ones, `"overwrite"` writes over them, `"fill_empty"` leaves\nthem. `conflicts.fields` gives one field its own. A number and the field its unit or currency is read\nfrom on each row are one cell, under the number\'s policy: both are written, or both held.\n\nA link cell takes `{ lookup: "<text>" }` in place of record ids: the linked row is the one row holding\nthat text in the linked table\'s unique or autonumber field, else the one titled it, among the rows the\nrun reads of that table. A text naming no row, or several, holds the row, and the rows it could be are\nanswered beside it. A link holding many rows takes a list, `[{ lookup: "<text>" }, \u2026]`: each text is\nresolved by itself.\n\nThe step keeps nothing of its own: its answer is everything it did. `written` lists each record it\nwrote \u2014 a changed one with each field from old to new as text, a created one by its id alone \u2014 and\n`held_rows` each row it held: its `kind`, `reason` and `message`, the `record_id` of the row it matched,\nits `key`, the `changes` it would make, the `links` it could not resolve with their `candidates`, and\n`values` \u2014 its key and the cells it would write, as the call gave them. A later call given a held row\'s\n`values` under `conflicts: { default: "overwrite" }` writes it. Both lists are read under the run\'s own\nreach of the table, and a workflow hands its caller only what it returns. Two calls matching one table\nby the same fields take turns, so the second matches the rows the first added. The step\'s entry in\n`workflow_steps` states its bounds.\n\nA workflow does not put an import back: that reads the history of what the run wrote, which a member\'s agent reads\nunder the member\'s own access to the table and no workflow does. On chat, MCP or the CLI, `undo_record_changes` takes the\ntable, up to 500 record ids and the run \u2014 what `runtime.execution_id` held in it, which a body keeps on a row of its own \u2014\nand puts back what that run wrote, as the member\'s own writes. A field goes back where the row still holds what the run\nwrote. A row the run created goes to the trash where nothing else has written it or linked a row to it since; in a table\nwhere a workflow writes each new row, every row created reads as written since, and is kept. A row the run moved to the\ntrash comes back where nothing has touched it since. A row the run brought back from the trash stays out of it. What the\ntool leaves as it stands is answered in `kept`, one entry a record and reason. `undo_record_change` puts back one edit\nnamed by its audit-log entry, the same way.\n\nWhat a business keeps of an import \u2014 the file, its counts, the lines waiting for a person \u2014 is rows of\nits own tables, written by the workflow that ran the step. A garden centre brings its grower\'s price\nsheet into its plants, and keeps each sheet and its lines. A line names its plant through a link\nholding many rows: one holding a single row would make the line that plant\'s own, and a plant its\nlines belong to is not moved to the trash.\n\nThese tables are shown beside the plants, in the same app: the sheets as a log of the plants\' register\n(a further List at the root, `model/app-node`), a sheet\'s lines on the page it opens. A line keeping what a cell held\nand what it holds now in two fields reads one against the other (`compare`, `model/list-node`).\n\n```\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: trigger.app_workflow.inputs.file,\n columns: ["Code", "Name", "Price"],\n});\n\nconst imported = await import_rows({ // a workflow step\n table_id: "plants",\n match: ["code"],\n rows: sheet.rows.map((row, at) => ({\n line: at + 2,\n hold: isNull(row["Price"]) ? "The sheet states no price" : null,\n values: {\n code: toString(row["Code"]),\n name: toString(row["Name"]),\n price: toNumber(row["Price"]),\n },\n })),\n});\n\nconst kept = await create_records({\n table_id: "price_sheets",\n records: [{\n file: [trigger.app_workflow.inputs.file],\n run: runtime.execution_id,\n read: imported.read,\n created: imported.created,\n changed: imported.changed,\n held: imported.held,\n }],\n});\nconst sheet_id = kept.record_ids[0];\n\nfor (let start = 0; start < size(imported.written); start += 50) {\n await create_records({\n table_id: "sheet_lines",\n records: slice(imported.written, start, start + 50).map((one) => ({\n sheet: [sheet_id],\n plant: [one.record_id],\n line: one.line,\n outcome: one.kind,\n changes: join(one.changes.map((change) => `${change.label}: ${change.before ?? "\u2014"} \u2192 ${change.after ?? "\u2014"}`), "; "),\n })),\n });\n}\nfor (let start = 0; start < size(imported.held_rows); start += 50) {\n await create_records({\n table_id: "sheet_lines",\n records: slice(imported.held_rows, start, start + 50).map((one) => ({\n sheet: [sheet_id],\n plant: isNull(one.record_id) ? [] : [one.record_id],\n line: one.line,\n outcome: "held",\n why: one.message,\n code: toString(sheet.rows[coalesce(one.line, 2) - 2]["Code"]),\n name: toString(sheet.rows[coalesce(one.line, 2) - 2]["Name"]),\n price: toNumber(sheet.rows[coalesce(one.line, 2) - 2]["Price"]),\n changes: join(one.changes.map((change) => `${change.label}: ${change.before ?? "\u2014"} \u2192 ${change.after ?? "\u2014"}`), "; "),\n })),\n });\n}\n\nreturn({ status: "success", message: `${imported.created} added, ${imported.changed} changed, ${imported.held} held` });\n```\n\nA held line keeps the cells the sheet states for it, found by the `line` the step answered, in\nfields of its own. A held row\'s `key` and `values` name fields by their keys, as every record a step answers a body does, so\nthe body reads the sheet again rather than those names. It is applied by the act on it, whose workflow hands the step those values again and\nlets it write over what the plant holds. A cell the line holds none of is left out, so a held line\nnever empties a cell of the plant:\n\n```\nconst line = await get_record({ table_id: "sheet_lines", record_id: trigger.app_workflow.inputs.record_id });\nconst values = {\n code: line.data["code"],\n ...(isEmpty(line.data["name"]) ? {} : { name: line.data["name"] }),\n ...(isNull(line.data["price"]) ? {} : { price: line.data["price"] }),\n};\n\nconst applied = await import_rows({ // a workflow step\n table_id: "plants",\n match: ["code"],\n conflicts: { default: "overwrite" },\n rows: [{ values }],\n});\nif (applied.held > 0) {\n return({ status: "error", message: applied.held_rows[0].message });\n}\n\nawait update_records({ table_id: "sheet_lines", record_ids: [line.id], set: { outcome: "applied" } });\nreturn({ status: "success", message: "Applied." });\n```\n\nA sheet is put back by a member\'s agent on chat, MCP or the CLI: `undo_record_changes` over the plants its lines name,\n500 at a time, with the run the sheet kept.\n\nA shop matches a bank statement to the notes it is owed by an act that states `review`. That act\'s workflow only\nproposes: its body calls steps that read and no other, and runs no agent \u2014 either is refused when the body is saved \u2014\nand returns in `data.review` one changeset of the act\'s table, which the platform writes the approved rows of. Called\nwith what the act asks, it answers the proposal and nothing is written; called again with\n`approved: { updates: [{ record_id, sha }], creates: [{ key, sha }] }`, the body runs again and the platform writes\nthose rows as this second run proposes them, so the body never reads `approved`. The platform adds a `sha` to every row\nof the proposal it answers \u2014 the body states none, and one it states is dropped \u2014 and the caller echoes the `sha` of\neach row it approves. A row the second run proposes with other values than the `sha` was read from is left unwritten\n(`changed`), so the body must propose the same for the same inputs and the rows it reads.\nAn update is of a record the act is run over (`record_id`, or one of `record_ids`): one of any other record is left\nunwritten. An act over every row in view is handed no rows: its body reads the rows it may change, and any of them may\nbe proposed. A create carries a `key`, a text of the body\'s own that names the same line on every run\n(`key: toString(line["Reference"])`), unique among the creates; where a confirm repeated must not add a second row,\nthe body also writes the key into a field the table keeps unique, so the repeat\'s create is left as a `conflict`.\nEach row\'s `data` holds only stored fields that change \u2014 never one the platform computes or stamps, nor a file \u2014 with\na `note` and a `ticked` the reader starts from, and `unplaced` lists what was read and matched nothing. Every approved\nrow is checked against its fields before any is written, and they are written in one transaction, so a flawed one\nwrites none. The status history of every row written that moved, or was made holding a status, is recorded in the\nsame run. The answer\'s `applied.previous` holds what each updated record\'s written fields held before, so one `update_records` of it puts the values back, and `delete_records` of a `created` record takes that row away. A body that stops proposing a row once it is\nwritten \u2014 it matches against what the rows hold now \u2014 makes approving twice write once.\n\n```\nconst inputs = trigger.app_workflow.inputs;\n\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: inputs.statement,\n columns: ["Reference", "Amount", "Paid on"],\n});\nconst unpaid = await query_records({\n table_id: "notes",\n filters: { field_key: "paid_on", operator: "is_empty" },\n pagination: { offset: 0, limit: 500 },\n});\nconst acted = groupBy(inputs.record_ids, (id) => toString(id));\nconst owed = filter(unpaid.records, (one) => size(acted[toString(one.id)] ?? []) > 0);\nconst byRef = groupBy(owed, (one) => toString(one.data["ref"]));\n\nconst lines = sheet.rows.map((line) => ({\n line,\n note: first(byRef[toString(line["Reference"])] ?? []),\n}));\nconst updates = filter(lines, (one) => !isNull(one.note)).map((one) => ({\n record_id: toString(one.note?.id),\n note: `Transfer of ${toString(one.line["Amount"])}`,\n ticked: toNumber(one.line["Amount"]) == sumBy(filter(owed, (owing) => owing.id == one.note?.id), (owing) => coalesce(owing.data["amount"], 0)),\n data: { paid_on: toString(one.line["Paid on"]) },\n}));\nconst unplaced = filter(lines, (one) => isNull(one.note)).map((one) => ({\n title: `${toString(one.line["Reference"])}: ${toString(one.line["Amount"])}`,\n note: "Matches no unpaid note",\n}));\n\nreturn({\n status: "success",\n message: `${size(updates)} of ${size(lines)} transfers match a note`,\n data: { review: { version: 1, table_id: "notes", creates: [], updates, unplaced } },\n});\n```\n\n### Saving a record with its lines\n\n`save_record`, a workflow step, saves one record and the rows of other tables that hang off it, all or\nnothing, and recognises what exists by the identity the input carries \u2014 so a call sent twice updates and\nadds nothing. `match` is the record\'s identity, `{ field_key: value }`, and must be exactly a uniqueness\nits table declares: one field marked `unique`, or the fields of one of the table\'s `unique` sets together\n(text, number, date, single select and one-row link fields). A `match` that is no declared uniqueness is\nrefused, naming what the table does declare. A live record carrying the match is found and only the cells\nof `set` that differ are written; none is found and it is added with `match` and `set`. `add_to` appends\nto a files, multi-select or many-link field and keeps what it holds.\n\n`lines` saves rows of other tables that link the record. Each entry names the child `table_id`, its\n`link` \u2014 a field of the child holding one row of this table, which the step fills and no row states \u2014\nthe child fields that, with `link`, are one uniqueness of the child table (`match`), and `rows`, each\nstating every `match` field. A line found by its match changes only the cells that differ, one not found\nis added, and a line the call does not list is left as it is: the step deletes nothing. Two rows of one\nentry cannot share a match.\n\nEverything is checked before the first write, and everything is written in one transaction: a table check\nthat refuses one line, a locked record or line the call would change, a value a field does not take, or a\ntable the run may not write refuses the call and saves nothing, naming the row. Two calls of one identity\ntake turns, so the second finds what the first saved. The answer is `record_id`, `created`,\n`updated_field_keys` (the cells changed on a record that existed) and, per entry of `lines`, the ids it\nadded, changed and left unchanged; these ids are typed for their tables, so a declared `record_link`\noutput or a follow-up write links straight to them.\n\n```\nconst inputs = trigger.app_workflow.inputs;\n\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: inputs.release_order,\n columns: ["Size", "Quantity"],\n});\n\nconst saved = await save_record({ // a workflow step\n table_id: "release_orders",\n match: { number: inputs.number },\n set: { note: "Read from the paper" },\n lines: [{\n table_id: "release_lines",\n link: "order",\n match: ["size"],\n rows: sheet.rows.map((row) => ({\n size: toString(row["Size"]),\n quantity: toNumber(row["Quantity"]),\n })),\n }],\n});\n\nreturn({\n status: "success",\n message: `${size(saved.lines[0].created_ids)} lines added`,\n data: { record_id: saved.record_id },\n});\n```\n\n## Reaching a connected service\n\nA service is reached only by a workflow step, and a step is called inside an app\'s workflow or an automation. From\nnothing to an app\'s run:\n\n1. `get_workspace` with `kind: "connection"` lists each `connected_account_id`.\n2. `set_app` declares a Workflow node that states what a call takes and hands back:\n\n```json\n{\n "type": "Workflow",\n "key": "invoice_order",\n "description": "Issues the KiotViet invoice for a confirmed order and hands back its code.",\n "inputs": { "order": { "type": "record_link", "entity": "order" } },\n "outputs": { "invoice_code": { "type": "text" } }\n}\n```\n\n3. `set_app_workflow` with the app, the alias `invoice_order` and this `source` (the step\'s entry is in `workflow_steps`):\n\n```\nconst order = await get_record({ table_id: "orders", record_id: trigger.app_workflow.inputs.order });\nconst made = await kiotviet_create_invoice({ // a workflow step\n connected_account_id: "cac_\u2026",\n branch_id: 1,\n sold_by_id: 7,\n lines: [{ product_id: 42, quantity: toNumber(order.data["quantity"]), price: toNumber(order.data["price"]) }],\n});\nreturn({ status: "success", message: "Invoiced", data: { invoice_code: made.invoice.code } });\n```\n\n4. `run_app_workflow` with `dry_run: true`, then the same call without it.\n\nA step wants the provider\'s own ids (`branch_id`, `sold_by_id`, `product_id`) and no tool lists them: declare a Workflow\nwhose body calls the provider\'s `list_*` workflow step (`kiotviet_list_branches`), run it, and read the ids from its answer.\nNo step lists KiotViet\'s users: read `sold_by_id` off a row `kiotviet_list_invoices` returns (KiotViet\'s `soldById`), or ask the owner.\n\n## Table checks\n\nA table check is one condition its table\'s record writes must not meet, declared beside the table\'s\n`unique`. It is not a workflow: it reads the write and a few lookups and answers yes or no, so it\ncannot change data. It runs inside the write, on every create, update or delete it is `on`, whichever\nsurface makes it \u2014 a person, the API, the CLI, an agent, a workflow. A refusal writes nothing and\nanswers with the check\'s message, under its field when the check names one, and names the check.\n\nA check is stated in its table\'s document, under `checks`, each under an alias of its own, and\n`set_tables` writes it with the rest of the table: a merge patch edits one, `null` removes it, and a\ncall that makes a table can state its checks, a lookup naming another table of the call by its `ref`.\n`get_table` states a table\'s checks in its document.\n\n| part | what it holds |\n|---|---|\n| `on` | the writes it checks: any of `create`, `update`, `delete` |\n| `when` | optional expression; the check runs only on a write where it is truthy. It cannot read lookups. |\n| `lookups` | optional, by name, the rows to read: `{ table, filter, limit }`, `table` an id or the `ref` of a table of the call, the filter as `query_records` takes it with fields and select options named by alias (`id` matches a row by its own record id), `limit` from 1 to 50 (50 when left out). A filter value that reads the record is `{ "$expr": "record.code" }`. |\n| `fail_when` | expression; truthy refuses the write |\n| `message` | expression for the refusal\'s text \u2014 a fixed text is a quoted string |\n| `field` | optional; the alias of the field the refusal is shown under |\n| `enabled` | `false` keeps the check without deciding writes; absent, it decides |\n\nEvery expression is written in the grammar above, names a table\'s fields and select options by\nalias (`record.status == "closed"`), and reads:\n\n- `record` \u2014 the record as it will be stored; on a delete, the record being deleted.\n- `prev_record` \u2014 on an update or a delete, the record before.\n- `changes` \u2014 on an update, the fields it changes. `!isNull(changes.photos)` asks whether a field changed.\n- `lookups.<name>` \u2014 in `fail_when` and `message`, the rows that lookup read. A row reads as `record` does: its fields by alias beside `id` (`row.start_on`), and a lookup field as the flat list of what it looks up (`first(row.booking_start)` through a one-row link). A lookup over the written table never returns the row being checked, and finds its other rows as the write leaves them, this call\'s other rows included. A field given to a helper as text names it by alias too (`sumBy(lookups.paid, "amount")`); text that names no field of the rows\' table is refused when the check is saved.\n- `runtime.timezone`, `runtime.workspace_id`, `runtime.organization_id`, `runtime.change_origin` (the\n write\'s origin), `runtime.triggered_by_member_id` (the member writing; null for a write no member\n made), `now()`, and `member_of("grp_\u2026")` \u2014 true when the member writing belongs to the group.\n\nIn a check a select reads as its one value (`record.kind == "retail"`), and a link (a one-row link too) and a lookup read as lists (`first(record.customer)`).\n\nA check on several operations reads only what all of them carry, so one on creates and updates reads\nneither `prev_record` nor `changes`.\n\nA write that recomputes another table\'s rows (a rollup over lines, a lookup or a formula over a parent) judges each recomputed row as an update, by the checks `on` `update` that read a field the recompute changed; `changes` is what the recompute changed. A check that refuses on `changes` or on a status the row holds can therefore refuse the write that moved the parent, so it is gated on the field it means (`!isNull(changes.status)`).\n\nA lookup reads with the access of the member who last saved the check, so saving one requires reading\nevery table its lookups read. It reads committed rows: two writes at the same moment can each pass a\ncheck the other would fail, so values no two rows may share are the table\'s `unique`, not a check.\n\nA `when`, a lookup or a `fail_when` that cannot decide \u2014 it fails, or the write\'s time runs out \u2014\nrefuses the write, naming the part it was reading and, for an expression or a filter, what it met\n(`lookups.clash: Filter on "start_on": date \'before\' value \u2026 is not a date`). A draft is checked when it is submitted, as a create; its edits before that are\nnot. A restored record is checked as a create. Cloning a table copies its checks to the member who\nclones it, and is refused when that member cannot read a table a check\'s lookup reads.\n\nOne open order per reference, a row never counting against itself:\n\n```json\n{\n "tables": [\n {\n "ref": "orders",\n "label": "Orders",\n "fields": {\n "reference": { "type": "text", "label": "Reference" },\n "closed_at": { "type": "date", "label": "Closed at" }\n },\n "checks": {\n "one_open_order_per_reference": {\n "on": ["create", "update"],\n "when": "isNull(record.closed_at)",\n "lookups": {\n "same_ref": {\n "table": "orders",\n "filter": {\n "node_type": "group",\n "logic": "and",\n "children": [\n { "node_type": "condition", "field_key": "reference", "operator": "equals", "value": { "$expr": "record.reference" } },\n { "node_type": "condition", "field_key": "closed_at", "operator": "is_empty" }\n ]\n },\n "limit": 2\n }\n },\n "fail_when": "some(lookups.same_ref, (r) => r.id != record.id)",\n "message": "\\"An open order already has this reference.\\"",\n "field": "reference"\n }\n }\n }\n ]\n}\n```\n\n## Table workflows\n\nA table workflow is what follows every committed write of a table\'s rows, whoever makes it: the grid, an import, an agent or\nany app. An effect that must hold for all of them belongs to the table. A button on one screen is that app\'s act, a value fixed\nwhen a row is made is `default_from`, and a rule that refuses a write is a check; a table workflow runs after the write has\ncommitted and cannot refuse it.\n\nIt is stated in the table\'s document, under `workflows`, each under an alias of its own, and `set_tables` writes it with the\nrest of the table: a merge patch edits one, `null` removes it, and `get_table` states a table\'s workflows in its document, with\nthe failures of the last 7 days beside them, to whoever manages the table. A dry run of `set_tables` with a `record_id` says what\neach workflow the call adds or changes would write on that row; reads are real and nothing is written.\n\n| part | what it holds |\n|---|---|\n| `on` | the write it follows: `create`, `update` or `delete` |\n| `when` | optional expression over `record`, `prev_record` and `changes`; the workflow runs only after a write where it is truthy |\n| `changed` | optional, with `on: update`: field aliases; the workflow runs only after an update that changes any one of them |\n| `body` | the steps, written as an app workflow\'s body is |\n| `enabled` | `false` keeps the workflow without running it; absent, it runs |\n\n```json\n{\n "tables": [\n {\n "ref": "repairs",\n "label": "Repairs",\n "fields": {\n "status": { "type": "select", "label": "Status", "options": [{ "alias": "queued", "label": "Queued" }, { "alias": "fixed", "label": "Fixed" }] },\n "fixed_on": { "type": "date", "label": "Fixed on" }\n },\n "workflows": {\n "stamp_fixed": {\n "on": "update",\n "changed": ["status"],\n "when": "record.status == \\"fixed\\"",\n "body": "await update_records({ table_id: \\"repairs\\", record_ids: [trigger.record_id], set: { fixed_on: formatDate(now(), \\"yyyy-MM-dd\\") } });"\n }\n }\n }\n ]\n}\n```\n\nThe body names the table\'s fields, select options and other tables by alias, and reads the write as `record`,\n`prev_record` and `changes` and the row\'s id as `trigger.record_id`. It operates on records and integrations: a body\nthat calls a tool changing a table, an app or any other structure is refused when it is saved. It runs as the table\'s owner, so\nit reaches the tables the owner reaches, and its writes meet the table\'s checks like any other write; a write refused by a\ncheck fails that run and leaves the row as it was. A body names a field by alias where it can tell which table\'s field it is: a\nread of `record`, `prev_record` or `changes`, a row from `get_record` or `query_records` called with a literal `table_id`, and a\nloop over such rows. A name it cannot tie to a table, and an option compared to a value of unknown type (not a read of a field, and\nnot a text, number or other value the type proves holds no option), are refused with what to change. A link reads as the list of the row ids it holds, a one-row link too: compare `first(record.customer) == id`. A lookup reads as one flat list of what it looks up: `record.x != prev_record.x` on one is refused, so compare `first(record.x)` with `first(prev_record.x)`. A single member, a single select, a number or a text reads as the value itself: `first(record.technician)` is refused at save, naming the read.\n\nA write the workflow itself makes sets off the workflows of its table in turn, and a chain of them stops after five. A workflow\nthat writes the row it follows is gated by a `changed` or a `when` that its own write does not satisfy. One run is queued for a\nrow and a workflow at a time, so a burst of writes to one row runs it once more after the run in flight. A field a workflow\nreads cannot be deleted or retyped until the workflow is changed or removed in the same call or before. Cloning a table copies its\nworkflows for whoever manages it (`include_workflows: false` leaves them), each with a body of its own, and is refused when a\nbody names a table the cloner cannot reach. A workflow runs as the table\'s owner, so whoever saves or rehearses one must manage the\ntable and read every table it reads in full: the body\'s, the gate\'s links\', the written table. An option is written where\nit is compared to, listed against or set on its field; a constant holding one is refused.\n\nA lock is the table\'s: a table workflow calls `lock_records` on a row (an app\'s act may too), and every door then refuses\nthe row\'s writes until whoever manages the table calls `unlock_records` \u2014 in an app\'s run, judged as the member who pressed.\nA locked row reads locked in every app. An app\'s own Check or `edit: false` only makes a row read-only in that app.\n\n## Runs\n\nEach start of a workflow is a run: an app\'s workflow or act, a table workflow, a button, an automation. An agent finds,\nreads, retries and stops runs on chat, MCP and the CLI. A run is read by whoever manages what its workflow belongs to: its\nautomation or its table, and for an app\'s workflow the app\'s owner or an admin, never a manager of the app.\n\n- **Find.** `query_workflow_executions` lists one workflow\'s runs (`workflow_id`), every run started for one row\n (`record_id`) \u2014 a table workflow\'s written row, a pressed button\'s row, an app run\'s `record_id` input \u2014 or both, newest\n first, a page at a time (`next_cursor`). Each run names its workflow and kind, what started it, its row and how it ended:\n for a failure, the step that threw, its tool and the error; for a waiting run, when it wakes, or the approval and the\n approvers it waits on, or the payment. A run started before runs recorded their row carries no `record_id`.\n- **Read.** `get_workflow_execution` gives one run\'s steps in order with each one\'s tool, input, output and error, what it\n returned, how many rows it wrote (a row\'s own history is in `query_audit_logs`) and the runs its writes started. Ninety days\n after a run starts, its trigger and its steps\' input and output are pruned; the read says so and gives the rest.\n `run_app_workflow` answers each run it makes with its `execution_id`.\n- **Retry.** `retry_workflow_execution` runs a failed, refused or cancelled run again as a new run linked to the first\n (`retry_of`): the first run\'s trigger and row, the workflow\'s current steps and its owner\'s current access, with the\n member who retries it as the one who started it. A run is retried once; a retry that fails is retried in turn. It asks\n first when the run, or a run it retried, already wrote rows or called a service no delete reverses. An automation\'s or a\n table workflow\'s retry runs in the background, as its runs do, and answers `queued`; a button\'s and an app\'s run while\n the call waits. An app\'s call is checked again against the workflow\'s current inputs. A pruned run, a run of a workflow\n that is off, a reviewed act\'s run (call the act again) and a recording\'s filing (the recording retries its delivery)\n are not run again.\n- **Stop.** `cancel_workflow_execution` stops a waiting run and withdraws its pending approvals.\n\nOff means off: a waiting run whose workflow was turned off, deleted, or taken off its automation, table, button or app ends\n`cancelled` when it would resume, saying why, and runs no further step.\n\nA waiting run keeps its version: when it resumes, it runs the steps of the version it started with, so an edit saved while it\nwaited changes later runs only. When that version is gone, the run ends failed and says why.\n';
1792
+ var workflows_default = '# Workflows \u2014 the steps a workflow runs\n\nA workflow is written as a strict subset of JavaScript. The source is never run as JavaScript: a\nsave parses it into steps, type-checks it against what its trigger supplies, and stores the steps.\nOnly the forms below are accepted; anything else is refused at save with the line, the column and\nwhat to write instead. A save that succeeds may still return `warnings` \u2014 advisory hints such as a\nloop that may never end. Read them.\n\nA workflow changes records, calls integrations, notifies and makes papers. A tool for anything else \u2014 views, history,\ntables and apps, members, knowledge, skills, editing a file \u2014 is an agent\'s, on chat, MCP or the CLI, and a body that\ncalls one is refused at save with that reason. One saved before is refused at run, before its first step.\n\nThe same grammar serves an app\'s workflow, which `set_app_workflow` saves, an automation and the expressions of a table\ncheck. An app workflow takes its trigger from the app that calls it; only an automation\'s source opens with a trigger\ndeclaration.\n\n## Triggers\n\nAn app workflow reads its call as `trigger.app_workflow.inputs.<name>`, or `inputs.<name>`, which is the same read. A Workflow node receives the inputs it declares;\nan Act\'s `workflow` receives `record_id` (`record_ids` on an act over several rows) and what the Act asks.\n\n### Automations\n\nAn automation answers an event outside the records: a schedule, a webhook, an inbound email. `set_automation` saves one\nfrom its source \u2014 the `on({...})` declaration, then its steps \u2014 and, given its `automation_id`, replaces it: the\ntrigger config is the one the header states, a key it leaves out removed, and steps left as it runs them are checked again\nonly when the call turns it on, so one a later rule refuses can still be switched off. `get_automation` reads that source back with its latest\nruns, and `dry_run_workflow` rehearses a source before it is saved. It runs with its owner\'s current permissions, a connected service it names is its owner\'s account, and only its\nowner or an admin reads, changes or deletes it.\n\n```\non({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });\n```\n\nEvery source below is typed, so a misspelled member is refused at save rather than read as nothing.\nAll three carry `trigger.trigger_id`.\n\n| trigger | required config | what the body reads |\n|---|---|---|\n| `recurring_schedule` | `cron_expression` | `runtime.timezone`; the current time is `now()` |\n| `receive_webhook` | `secret`? | `trigger.method`, `trigger.headers[...]`, `trigger.query[...]`, `trigger.body` (any JSON, or raw text when the request is not JSON), `trigger.received_at` |\n| `receive_gmail_email` / `receive_outlook_email` | `connected_account_id`, `filter` | `trigger.email_id`, `trigger.from`, `trigger.to`, `trigger.cc`, `trigger.bcc`, `trigger.subject`, `trigger.date`, `trigger.body` (plain text), `trigger.reply_to`, `trigger.in_reply_to`, `trigger.attachments` (file refs \u2014 assign them straight to a files field). One type serves both providers, so `labels`, `thread_id`, `importance` and `conversation_id` are not readable. |\n\n## Steps\n\n| step | syntax |\n|---|---|\n| tool call, kept | `const <id> = await <tool>({ ...inputs });` \u2014 also `let x = await \u2026` and `x = await \u2026` |\n| tool call | `await <tool>({ ...inputs });` \u2014 the platform\'s own steps (`send_notification`, `generate_document`, \u2026) and a connection\'s tools, each a workflow step (`bkav_submit_invoices`, `kiotviet_create_order`, \u2026); a step that fails ends the run with its reason unless a `try`/`catch` around it handles it |\n| agent | `const <id> = await agent({ instructions, input, model, output });` \u2014 see **An agent step** |\n| app agent | `const <id> = await app_agent({ alias: "<agent alias>", input: { ... } });` \u2014 runs one of the app\'s declared agents, in an app workflow only, and resolves to its declared outputs or its final text. `wait: false` only starts the run and resolves to `{ run_id }`; the run\'s failure is then its own, not the workflow\'s. Refused in a run an agent started. |\n| bind | `const <id> = <expression>;` \u2014 evaluated once; later steps read `<id>`. Bind any expression used twice. |\n| if / else | `if (<expr>) { ... } else { ... }` \u2014 `else` optional, `else if` chains allowed |\n| switch | `switch (<expr>) { case "X": { ... } default: { ... } }` \u2014 string cases only, no fallthrough |\n| for | `for (const <name> of <expr>) { ... }` |\n| wait | `await wait({ duration_in_minutes: 5 });` |\n| wait for an event | `await wait_for_event({ event_type: "webhook", event_ref: <expr>, timeout_in_minutes: 60 });` |\n| wait for an approval | `await wait_for_approval({ approvers: <expr>, prompt: <expr> });` \u2014 see **Waiting for an approval** |\n| return | `return({ status: "success" \\| "error", message: <expr>, field_errors: { ... }?, data: { ... }? });` \u2014 `data` holds the outputs a Workflow node declares |\n| validate | `validate({ checks: [{ fail_when: <expr>, field_key: "...", message: <expr> }, ...] });` |\n\nA delayed run is never cancelled from a workflow: after a `wait`, it reads the row\'s state and acts only if it still holds.\n\nA tool\'s inputs and answer are in its own description; the steps only a body runs \u2014 a connection\'s, a mail\'s \u2014 are each\nan entry of `workflow_steps`. A person connects a service in Lotics under Settings \u2192 Connections; a step names that\nconnection by its `connected_account_id`, which `get_workspace` with `kind: "connection"` lists for the connections the\ncaller can use. The app\'s owner and whoever saves the body must both be able to use that connection.\n\nIn a workflow, email reaches a member of the workspace through the step `send_notification`: Lotics shows it to them and\nmails it to their own address, with no connection. Its recipients are members by `member_id` (`get_workspace` with\n`kind: "member"` lists them), member groups or the whole organization. Mail to anyone else goes from a person\'s\nconnected Gmail or Outlook (`gmail_send_email`, `outlook_send_email`), which leaves an unsent draft unless `draft` is\n`false`.\n\n`return` is a call, not a JavaScript `return` statement. `validate` ends the workflow with an error\nwhen a check\'s `fail_when` is truthy; the check\'s `field_key` names the control its message lands on \u2014\nin an app workflow a declared input, or the alias of a field on a table the body names; in an\nautomation it is not checked.\n\nThe name in `const x = await tool({...})` is the step\'s id; later expressions read its output as\n`x.records`, `x.id` and so on. A `// id: my_step` comment directly above a statement sets an explicit\nid; any other `//` comment there becomes the step\'s description.\n\n**`await` inside a statement** runs the call as its own step first, then reads its output where it is\nwritten: `size((await query_records({...})).records)`, an `if` test, a `return` value, a `for-of`\niterable, a tool input. `const x = c ? await t({...}) : null;` is stored as the `if` it means. An\n`await` that would run on some paths only \u2014 inside `&&`, `||`, `??`, `?.`, a nested ternary, a loop\ntest, a `validate` check \u2014 is refused; write the `if`.\n\n**Names are block-scoped, as in JavaScript** \u2014 `const`, `let`, loop items and `catch (e)`: sibling\nblocks may reuse a name and an inner block may shadow an outer one. A local may take a helper\'s name\n(`const size = 3;`), but calling `size(...)` while it is in scope is refused. The names of the tools a\nworkflow calls, the reserved roots, `linked` and `_s` followed by digits cannot be bound.\n\n### Waiting for an approval\n\n`wait_for_approval` is the wait worth binding \u2014 `wait` and `wait_for_event` carry nothing to read.\nBind it to branch on the decision:\n\n```\nconst approval = await wait_for_approval({\n approvers: trigger.app_workflow.inputs.approvers,\n prompt: `Approve this quote?`,\n});\n\nif (approval.status == "approved") {\n // the approved path\n} else {\n // the rejected or timed-out path; approval.decision_comment says why\n}\n```\n\nIt resolves to `{ status: "approved" | "rejected" | "timed_out", decided_by: MemberId | null,\ndecided_at: ISO string, decision_comment: string | null }`.\n\n`approvers` takes a bare member id (`"mbr_\u2026"`), a bare group id (`"grp_\u2026"`) or a full principal\n(`{ type: "member_group", id: "grp_x" }`), alone or in a list \u2014 so a member field\'s value passes as it\nis.\n\n### An agent step\n\n`const x = await agent({ instructions, input, model, output });` runs a model as one step \u2014 read a paper, classify,\ndraft text. It calls no tool: the workflow\'s own steps write, send and notify with what it returns, and a step that\nstates `tools` is refused.\n\n- `instructions` \u2014 a fixed string.\n- `input` \u2014 an object literal; its fields are expressions, evaluated and handed to the model (`{}` for\n none). It is the ONLY workflow data the agent sees: read the trigger, prior steps, `runtime` or the loop\n item here, never inside `instructions`, which is a fixed literal and reads no binding. A field holding a file, or a\n list of files (a files field, a mail\'s attachments), hands the agent the files themselves: a picture or a PDF it\n sees, any other paper read into text.\n- `model` \u2014 optional; omit it to follow the platform\'s default model.\n- `output` \u2014 `{ mode: "text" }` resolves to a string; `{ mode: "object", schema }` to a typed object.\n\nOnly `x` comes back: in text mode `x` is the string itself, in object mode `x.field` reads a field the\n`schema` declares.\n\n## Naming tables and templates\n\nA body names a table or document template by its alias (`query_records({ table_id: "orders" })`); ids still work. `get_app_workflow` and `get_automation` show aliases. An app\'s workflow reaches only the tables the app uses.\n\n## Fields and options\n\n1. **A body names a table\'s fields and select options by alias**, the names the table\'s document states: a field reads\n as `record["status"]`, an option as `"done"`, a linked field as `linked(record["customer"])[0]["name"]`, in the\n body of a Workflow node, of an Act\'s own `workflow`, of an automation (`set_automation`) and of a table\'s workflow. It is stored by key and `get_app_workflow`, `get_automation` and `get_table` show it by alias\n again. A field\'s or option\'s key is accepted where its alias is and is kept as written; a display name is refused with the\n aliases there are, and a row that names one field by both its alias and its key is refused. A row written to a table is\n stored by field key wherever the save can follow it to the record tool that writes it: a row or list written in the call,\n a conditional of them, a name holding them, pushed to a `let`, and through `map`, `flatMap`, `concat`, `filter`, `sortBy`,\n `slice` and `reverse`. A field of such rows is read by alias as well (`picks.toSorted((a, b) => a.quantity - b.quantity)`,\n `sortBy(picks, "quantity")`, `rows[0].note`) and stored by key. A body that cannot be stored by key is refused at save,\n naming the statement.\n2. **An Act\'s own `workflow`** has the app\'s guards placed at the top of the body, between `<lotics:guards>` tags,\n replaced on every save: a save that leaves them out or edits them gets the app\'s again, at the top wherever the tags\n sat, so the act\'s row rule, `when`, `requires`, checks and locks always run first.\n3. **A value is read as the record tools store it.** A single select reads as its option and a member as its id; an\n option is written by its alias wherever it meets a select \u2014 compared, listed, switched on, written \u2014 also beside a\n name set from the field (`const s = record.status; s == "done"`), and a select\n value handed on whole (returned, written to a text field) is its key. An option\'s alias searched for in a list the\n save cannot place it against (`includes(filter(record.tags, \u2026), "hot")`) is refused: search the field itself. A link reads as the list of its row ids, a\n one-row link too: `first(record.customer)` is the one id, and `record.customer` used as one id is refused at save,\n naming `first(record.customer)`. A one-row link is written with its one id or a list of one; a many-row link with a list. A lookup reads as one flat list of the values it looks up: `includes(record.owner,\n me)`. A select\'s value is one of another select\'s options only where that table shares its keys, by having been\n duplicated from it, not where an option merely shares its alias. A row that takes its parent\'s option gets it from\n `default_from`, and a lookup shows it.\n4. **What an app sends takes an option by alias or key.** An input or output drawn from a field (`field`, written\n `<table id>.<alias>`) compares and returns by alias in the body, and the run hands it on as the option\'s key. A\n refusal\'s `field_errors` and a `validate` check\'s `field_key` name a declared input or a field\'s alias.\n5. **`==` against a multi-select means "includes".** `record["tags"] == "urgent"` on a multi-select is stored as\n `includes(record["tags"], "urgent")`; either spelling works.\n6. **Text renders labels.** Inside a template or a text tool input, a select, member or link value renders its display\n text: `` `${record["status"]}` `` writes "Done", not `done`.\n\n## What an expression reads\n\n- `record`, `prev_record`, `changes` \u2014 in a table check, the write it checks (see **Table checks**).\n `changes` is a PARTIAL map: `changes["x"]?.next_value` and `?.prev_value`, the `?.` required.\n- `trigger` \u2014 what the trigger carries (see **Triggers**).\n- a loop\'s item name, and `index`, the iteration\'s 0-based index.\n- `<step id>` \u2014 an earlier step\'s output (`dup.records`).\n- `runtime.timezone`, `runtime.workflow_id`, `runtime.execution_id`, `runtime.workspace_id`,\n `runtime.organization_id`, `runtime.change_origin`, `runtime.triggered_by_member_id`. The current\n time is `now()`.\n\n## Paths\n\n- `record.x` and `record["x"]` read a field; `[n]` reads a list\'s n-th item.\n- A path may start at any expression: an awaited call `(await get_record({...})).data["x"]`, a helper\'s\n result `first(found.records).id`, a filter `rows.filter((r) => r.ok)[0].n`, a `(a ?? b).c`. `?.` reads\n through a null as usual, except on a value drawn at random (`sample(xs)?.id` is refused: bind it first).\n- A record reads in its declared shape wherever it comes from \u2014 the trigger, a tool result, a `let`, a\n callback parameter: a single select as its option alias or null, a member as its id, a link as its ids. (The record\n tools called outside a workflow return the stored arrays instead.)\n- `linked(record["link"])[n]["x"]` fetches the n-th linked row and reads a field of it; bare\n `linked(record["link"])` is every linked row, one fetch per row. Its argument is a path ending on\n a link field and carries the guard: `linked(record?.["link"])[0]`.\n- `changes["link"]?.next_value` is the same list of ids; `linked()` over a change is refused \u2014\n read `linked(record["link"])` or `linked(prev_record["link"])`.\n\n## Operators and statements\n\nOperators in JavaScript precedence: `? :`, `||`, `??`, `&&`, `== != === !==`, `< <= > >=`, `+ -`, `* / %`,\nprefix `! -`.\n\n- Arithmetic is exact decimal (`0.1 + 0.2` is `0.3`). A result computed from a division that does not end\n is the short decimal it reads as (`1 / 3 * 3` is `1`), and stands as computed where it reads long\n (`1 / 3 * 100` is `33.33333333333333`). A comparison between two numbers reads both at 15 significant\n digits. `round` and `formatNumber` round the decimal, a half away from zero: `round(-2.5)` is `-3`. A\n table filter compares stored values exactly.\n- `??` is `coalesce(left, right)`; `?.` reads through a null (`a?.b`, `a?.[0]`, and `s?.trim()` for the\n method names listed under **Helpers**). `x?.()` is refused: helpers and tools are not values.\n- Templates use backticks and `${...}`.\n- Destructuring \u2014 `const { status, name } = record;` binds each name. Renames\n (`{ status: s }`), quoted keys (`{ "ref": ref }`), array patterns with holes\n (`const [first, , third] = xs;`) and defaults (`{ note = "" }`, which apply on null too) work on\n `const`, `let` and `for (const { id } of rows)`. A tool result destructures directly:\n `const { records } = await query_records({...});`. `const a = 1, b = 2;` declares both. Nested\n patterns and rest are refused.\n- Spread \u2014 `[...a, b]` and `{ ...a, b: 1 }` (later keys win). Refused inside a tool input: bind the\n merged value first and pass the binding.\n- Shorthand \u2014 `{ table_id }` is `{ table_id: table_id }`.\n- `xs.push(a, b);` appends, also on a key (`o.items.push(v)`); `o.a.b = v;` sets a nested key;\n `x ??= v`, `x ||= v` and `x &&= v` assign. A `const` may be pushed to or have a key set, as in\n JavaScript; a loop item is read-only.\n- `undefined` is the same value as `null`, so `x === undefined` is `isNull(x)` and `x !== undefined` is\n `isNotNull(x)`; a key set to null and a key never sent are not told apart (`"k" in o` tells a key that\n is there).\n- `typeof x` answers JavaScript\'s words, and a null (a missing value too) is `"object"`. `k in o` tests an\n object\'s own keys, or a list\'s indexes and `length`.\n- A filter node may leave out `node_type` when its keys say which it is: `field_key` / `operator` /\n `value` a condition, `logic` / `children` a group, `path` / `condition` a traversal.\n- `function name(p = <default>) { return <expr>; }` \u2014 top level only, expanded at every call (a call\n may come before it). Every parameter needs a default, which gives it its type; the body is one\n `return <expr>;` reading only its parameters, helpers and the reserved roots \u2014 never the caller\'s\n names, nor `index`. No recursion; a function never called is refused. A body read back shows the\n expression at each call, not the `function`.\n\n**try / catch.** `try { ... } catch (e) { ... }` catches a tool error or an expression error inside the\nbody; `e` is `{ message, type, step_id?, detail? }`. A failed `validate` and a `return` are not\nerrors \u2014 they end the workflow \u2014 and a step that runs after a wait inside the `try` is outside it.\n`finally` is refused: put always-run steps after the `try`.\n\n**Loops.** `for-of`, `while (cond) { ... }`, `do { ... } while (cond);` and\n`for (let i = 0; i < n; i++) { ... }`, each capped at 10,000 iterations. `break;` and `continue;` act\non the innermost loop; labels and `for-in` are refused.\n\n**`let`.** `let x = <expr>;` declares a block-scoped variable; the initializer is required\n(`let x = null;`). Reassign with `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `??=`, `++` and `--`. Re-declaring in\nthe same block is refused; shadowing in a nested block is allowed. A `let` keeps its value across a\n`wait`, `wait_for_event` or `wait_for_approval`.\n\n**Refused, each because a save must know what runs and a run must give the same answer twice:** regex\n(unbounded matching: use the string methods), `Math.random`, `Date.now()` and `new Date()` (use `now()` and\n`parseDate`), a tool or helper as a value or a computed callee (callees are named), `new` of anything,\n`instanceof`, `delete`, `void`, rest elements, computed keys, classes, `throw`, `finally`, `import`,\n`export`, `eval`, labels, and a function as a value (`const f = (x) => ...`: pass the arrow to the helper\nthat takes it). `xs.sort(...)` on a name or path is refused too, since a value is never changed in place:\nwrite `xs.toSorted(...)` or `[...xs].sort(...)`.\n\n## Typing\n\n- The body is type-checked as TypeScript before it is read: annotations, `as`, `satisfies` and a non-null `!`\n are accepted and erased, and the checker is strict about null. The stored body keeps none of them and is checked again as\n stored, by field key, as every later change to its app or tables checks it: a body that types only through one of them is\n refused at save.\n- Narrow what is not typed (a connector\'s JSON is `unknown`) with `typeof`, `in`, `Array.isArray` and `isNull`.\n- `first(xs)` may be undefined; `requireFirst(xs)` is the list\'s first item or an error.\n- `obj[key]` needs `key` to be a key of `obj`, or `obj` to be a record with a string index.\n\n## Helpers\n\nOrdinary JavaScript works \u2014 the standard library below, the array and string methods, `Math`, `JSON`,\n`Object`, `Array`, `Number`, `parseInt` \u2014 and means what it means in JavaScript, with the differences\nlisted here. Where JavaScript answers NaN, Infinity or undefined, a workflow has none of them and the\nanswer is `null`.\n\n- Standard library: `Math.abs/ceil/floor/trunc/sign/min/max/pow/sqrt`, `Number(x)`, `String(x)`,\n `Boolean(x)`, `parseInt(s, radix?)`, `parseFloat(s)`, `Number.isFinite/isInteger`,\n `Object.keys/values/entries/fromEntries`, `Array.isArray/from` (a list, text, or `{ length: n }`),\n `JSON.parse/stringify`.\n- Arrays: `length, map, filter, find, findIndex, findLast, some, every, reduce, includes, indexOf, join,\n slice, concat, flat, flatMap, at, reverse, toReversed, toSorted` and `[...xs].sort(compare?)`.\n- Strings: `length, toLowerCase, toUpperCase, trim, trimStart, trimEnd, startsWith, endsWith, includes,\n indexOf, slice, substring, split, replace, replaceAll, padStart, padEnd, repeat, charAt, at,\n localeCompare` (en-US order, wherever it runs). Numbers: `toFixed, toString`.\n- Differences from JavaScript: `Math.round` is the engine\'s `round` (a half goes away from zero:\n `Math.round(-2.5)` is `-3`); `Number(x)` and `parseFloat(x)` are `toNumber`, which refuses text that is no\n number; `String(null)` is `""`; `join` writes a null item as `null`; `concat` skips a null; `toFixed`\n rounds the decimal, not the binary double; `replaceAll` writes the replacement as text\n (`$&` stays as typed), while `replace` expands `$&`, `$$`, `` $` `` and `$\'` in it as JavaScript does; `split` takes no limit; `sort` without a comparator orders by text, as JavaScript does. `==`, `!=`, `===` and `!==` between two list values (files, a multi-select, a link of any cardinality, a lookup) are refused at save, since a list compares by identity, not by its items: `!isNull(changes.<field>)` asks whether one changed.\n\nThe same work can be written with the helpers below, which are additions to JavaScript. They are called as\n`size(arr)`; the method form works only where the name is also a JavaScript method \u2014\n`x.trim()`, `arr.includes(v)`, `arr.at(-1)`, `arr.map(fn)`, `s.split(",")`; `arr.size()` is refused.\n\n- **Null and type**: `isNull`, `isNotNull`, `isEmpty`, `isString`, `isNumber`, `isBoolean`, `isArray`,\n `isObject`, `coalesce`, `get`, `toNumber`, `toFigure`, `toString` (a text, number, yes/no or date; a record, file or list is refused at save, an object at run), `typeOf`, `parseJson`, `toJson`. `isNull` is strict (`null` and `undefined` only), but on a list field of the record (a link, files, a multi-select) it asks whether the list holds nothing, since a cleared list is `[]`: `!isNull(record.vehicle)` reads as `!isEmpty(record.vehicle)`. `isNull(changes.<field>)` stays strict: null there means unchanged\n- **Lists**: `size`, `first`, `requireFirst` (the first item, refusing an empty list \u2014 after a\n `validate` on the size it saves an `if`), `last`, `nth`, `at` (`at(arr, -1)` counts from the end),\n `includes`, `filter`, `find`, `some`, `every`, `pluck`, `sortBy`, `groupBy`, `countBy`, `unique`,\n `uniqueBy`, `compact` (drops falsy items), `flatten`, `reverse`, `slice`, `concat`, `difference`,\n `differenceBy`, `intersection`, `intersectionBy`, `list`, `reduce(arr, (acc, x) => ..., initial)`,\n `range(end)` / `range(start, end)`\n- **Math**: `sum`, `sumBy`, `mean`, `meanBy`, `minBy`, `maxBy`, `round`, `ceil`, `floor`, `min`, `max`,\n `abs`, `mod`, `pow`, `sqrt`, `clamp`, `percentage`\n- **Text**: `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`,\n `replaceAll`, `removeChars(str, chars)` (without every character that occurs in `chars`),\n `removeSpaces(str)` (without any whitespace; a Word template command calls both too), `substring`, `length`, `split`, `join`, `padStart(str, length, char)`,\n `padEnd(str, length, char)`, `formatNumber(x, decimals)` (ungrouped; rounds the decimal, a half away\n from zero: `formatNumber(1.005, 2)` is `1.01`),\n `formatDecimal(x, decimals, locale)` (grouped: `formatDecimal(151000, 0, "vi-VN")` is `151.000`),\n `numberToWords(x, lang?)` (an integer in words: Vietnamese, or English with `"en"`),\n `uuidFrom(text)` (the same UUID for the same text, refusing null and empty text: `uuidFrom(record.id)`)\n- **Objects**: `keys`, `values`, `entries`, `nonNullKeys`, `pick`, `omit`, `merge`\n- **Dates**, in the workspace\'s timezone: `now`, `formatDate`, `parseDate`, `addDays`, `subDays`,\n `addHours`, `subHours`, `addMinutes`, `subMinutes`, `startOfDay`, `endOfDay`,\n `differenceInCalendarDays`, `differenceInHours`, `differenceInMinutes`, `isBefore`, `isAfter`,\n `isSameDay`, `isToday`, `isWithinRange`, `meetsBounds`\n- **Other**: `formatCurrency(amount, locale, currency)`, `randomNumber(len)`,\n `randomAlphaNumeric(len)`, `sample(items)` (a random item)\n\nOver a null or empty list `mean`, `meanBy`, `min`, `max`, `minBy` and `maxBy` answer null, while `sum`\nand `sumBy` answer 0: `maxBy(pieces, "reach")?.Name` prints a blank for a month with no pieces.\n\nA date helper given a null or empty date returns null, so guard its result before comparing or\nwriting it: `differenceInCalendarDays(a, b) ?? 0`.\n\n`filter`, `find`, `some`, `every`, `sortBy`, `pluck`, `sumBy`, `meanBy`, `minBy`, `maxBy`, `groupBy`,\n`countBy`, `uniqueBy`, `differenceBy` and `intersectionBy` take a path string\n(`filter(record.items, "Status", "open")`) or a callback\n(`filter(record.items, x => x.Status == "open" && x.Amount > 100)`); `reduce` takes a callback and an\ninitial value. A callback gets `(item, idx)` (`reduce`: `(acc, item, idx)`) and reads `record`,\n`runtime`, the names in scope, loop items and an enclosing callback\'s parameters. Its body is one\nexpression \u2014 `(x) => <expr>`, `(x) => { return <expr>; }` or `function (x) { return <expr>; }`;\nseveral statements, `async` and named function expressions are refused.\n\nLook a row up by key rather than filtering for it: a `filter` inside a callback walks its whole list again\nfor every item, so a thousand items against a few hundred rows is hundreds of thousands of calls. Group the\nrows once, then read each group by its key. `groupBy` answers an object of lists, and `byCode[code] ?? []`\nis the group a code names, or an empty list:\n\n```\nconst parties = await query_records({ table_id: "parties" });\nconst lines = await query_records({ table_id: "sheet_lines" });\nconst byCode = groupBy(parties.records, (p) => p.data["code"] ?? "");\nconst holders = lines.records.map((line) => ({\n line: line.id,\n parties: (byCode[line.data["code"] ?? ""] ?? []).map((p) => p.id),\n}));\n```\n\nA key is read exactly: a code holding a dot, a slash or a bracket reads its own group, and a name every\nobject inherits (`constructor`) reads nothing unless a row made a group of that name. `get(o, "a.b")`\nfollows a dotted path instead, so a key that holds a dot is read with `o[key]`.\n\n## Writing a field: null and undefined\n\nIn `update_records`\' `set` and `create_records`\' `records`:\n\n- `null` clears the field.\n- `undefined`, or leaving the key out, leaves the field as it is.\n\nSo passing a read that may be null (`record["x"]`) keeps a value when there is one and clears the\nfield when there is none. Use `coalesce(x, fallback)` only for a real fallback value.\n\n## Examples\n\nCompare a link by id. Display text is not unique, and a link reads as a list of ids \u2014 `==` against a\nstring is a type error:\n\n```\nconst found = await query_records({\n table_id: "customers",\n filters: { node_type: "condition", field_key: "name", operator: "equals", value: "ACME Corp" },\n});\nconst customer = first(found.records);\nconst order = await get_record({ table_id: "orders", record_id: trigger.app_workflow.inputs.order_id });\n\nif (customer && includes(order.data["customer"], customer.id)) {\n // \u2026\n}\n```\n\nRead an uploaded sheet and add the rows a table lacks. `excel_get_rows`, a workflow step, answers every\nrow of an `.xlsx`, `.xls` or `.csv` sheet in one call, each an object keyed by the header row\'s text. A cell is a number,\n`true` or `false`, text, a date as ISO text, or null when blank, so `row["Header"]` is typed\n`string | number | boolean | null` and a helper narrows it: `toString` for text, `toNumber` for a number,\n`isString` before a date. Every cell of a `.csv` is text. `columns` keeps only the ones the body reads.\nA sheet past what one call reads is refused with its size, never cut short: the step\'s entry in\n`workflow_steps` states the bounds.\n\n`import_rows`, a workflow step, matches those rows to a table by a key and writes what differs. `match`\nnames the fields that identify a row, compared as trimmed text with case ignored: a row matching no live\nrow is added, one matching a row changes only the cells that differ, and one that changes nothing is\ncounted and not written, so the same file run twice writes nothing the second time. `values` holds a\nrow\'s cells by field alias, as `create_records` takes them; a key the row omits is left as the table holds\nit, and `null` is an empty cell, which differs from a filled one as any other value does. `dry_run: true` judges the import as a real one would be, rolls it back and answers what it would write or why it is refused.\n\nA `match` field may also be a link field holding one row, compared by the row linked: lines keyed by\ntheir parent and a line number state `match: ["invoice", "line"]`, the parent given as its\nrecord id or as `{ lookup: "<text>" }`.\n\nWhat the step is not certain of is held \u2014 answered with why, and not written: a row carrying `hold`\n(its text is the message); a row whose key several live rows carry, or an earlier row of the same call;\na row the table\'s checks refuse, or whose row is locked; with `create: false`, a row matching none. A\nheld row never fails the step, and the rows beside it are written. A row whose key names a row outside\nthe rows the run may write is held `refused` with nothing of that row: no `record_id`, no `changes`.\n\nThe step removes no row, and a row the source leaves out is never touched. A row carrying\n`remove: true`, with its key in `values`, proposes moving the row its key names to the trash: it is\nalways held, whatever `create` and `conflicts` say, and a key naming no row is counted unchanged.\n\n`conflicts` states what a matched row does where a live cell holds another value: `"hold"` writes the\nrow\'s empty cells and holds the differing ones, `"overwrite"` writes over them, `"fill_empty"` leaves\nthem. `conflicts.fields` gives one field its own. A number and the field its unit or currency is read\nfrom on each row are one cell, under the number\'s policy: both are written, or both held.\n\nA link cell takes `{ lookup: "<text>" }` in place of record ids: the linked row is the one row holding\nthat text in the linked table\'s unique or autonumber field, else the one titled it, among the rows the\nrun reads of that table. A text naming no row, or several, holds the row, and the rows it could be are\nanswered beside it. A link holding many rows takes a list, `[{ lookup: "<text>" }, \u2026]`: each text is\nresolved by itself.\n\nThe step keeps nothing of its own: its answer is everything it did. `written` lists each record it\nwrote \u2014 a changed one with each field from old to new as text, a created one by its id alone \u2014 and\n`held_rows` each row it held: its `kind`, `reason` and `message`, the `record_id` of the row it matched,\nits `key`, the `changes` it would make, the `links` it could not resolve with their `candidates`, and\n`values` \u2014 its key and the cells it would write, as the call gave them. A later call given a held row\'s\n`values` under `conflicts: { default: "overwrite" }` writes it. Both lists are read under the run\'s own\nreach of the table, and a workflow hands its caller only what it returns. Two calls matching one table\nby the same fields take turns, so the second matches the rows the first added. The step\'s entry in\n`workflow_steps` states its bounds.\n\nA workflow does not put an import back: that reads the history of what the run wrote, which a member\'s agent reads\nunder the member\'s own access to the table and no workflow does. On chat, MCP or the CLI, `undo_record_changes` takes the\ntable, up to 500 record ids and the run \u2014 what `runtime.execution_id` held in it, which a body keeps on a row of its own \u2014\nand puts back what that run wrote, as the member\'s own writes. A field goes back where the row still holds what the run\nwrote. A row the run created goes to the trash where nothing else has written it or linked a row to it since; in a table\nwhere a workflow writes each new row, every row created reads as written since, and is kept. A row the run moved to the\ntrash comes back where nothing has touched it since. A row the run brought back from the trash stays out of it. What the\ntool leaves as it stands is answered in `kept`, one entry a record and reason. `undo_record_change` puts back one edit\nnamed by its audit-log entry, the same way.\n\nWhat a business keeps of an import \u2014 the file, its counts, the lines waiting for a person \u2014 is rows of\nits own tables, written by the workflow that ran the step. A garden centre brings its grower\'s price\nsheet into its plants, and keeps each sheet and its lines. A line names its plant through a link\nholding many rows: one holding a single row would make the line that plant\'s own, and a plant its\nlines belong to is not moved to the trash.\n\nThese tables are shown beside the plants, in the same app: the sheets as a log of the plants\' register\n(a further List at the root, `reference/app-node`), a sheet\'s lines on the page it opens. A line keeping what a cell held\nand what it holds now in two fields reads one against the other (`compare`, `reference/list-node`).\n\n```\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: trigger.app_workflow.inputs.file,\n columns: ["Code", "Name", "Price"],\n});\n\nconst imported = await import_rows({ // a workflow step\n table_id: "plants",\n match: ["code"],\n rows: sheet.rows.map((row, at) => ({\n line: at + 2,\n hold: isNull(row["Price"]) ? "The sheet states no price" : null,\n values: {\n code: toString(row["Code"]),\n name: toString(row["Name"]),\n price: toNumber(row["Price"]),\n },\n })),\n});\n\nconst kept = await create_records({\n table_id: "price_sheets",\n records: [{\n file: [trigger.app_workflow.inputs.file],\n run: runtime.execution_id,\n read: imported.read,\n created: imported.created,\n changed: imported.changed,\n held: imported.held,\n }],\n});\nconst sheet_id = kept.record_ids[0];\n\nfor (let start = 0; start < size(imported.written); start += 50) {\n await create_records({\n table_id: "sheet_lines",\n records: slice(imported.written, start, start + 50).map((one) => ({\n sheet: [sheet_id],\n plant: [one.record_id],\n line: one.line,\n outcome: one.kind,\n changes: join(one.changes.map((change) => `${change.label}: ${change.before ?? "\u2014"} \u2192 ${change.after ?? "\u2014"}`), "; "),\n })),\n });\n}\nfor (let start = 0; start < size(imported.held_rows); start += 50) {\n await create_records({\n table_id: "sheet_lines",\n records: slice(imported.held_rows, start, start + 50).map((one) => ({\n sheet: [sheet_id],\n plant: isNull(one.record_id) ? [] : [one.record_id],\n line: one.line,\n outcome: "held",\n why: one.message,\n code: toString(sheet.rows[coalesce(one.line, 2) - 2]["Code"]),\n name: toString(sheet.rows[coalesce(one.line, 2) - 2]["Name"]),\n price: toNumber(sheet.rows[coalesce(one.line, 2) - 2]["Price"]),\n changes: join(one.changes.map((change) => `${change.label}: ${change.before ?? "\u2014"} \u2192 ${change.after ?? "\u2014"}`), "; "),\n })),\n });\n}\n\nreturn({ status: "success", message: `${imported.created} added, ${imported.changed} changed, ${imported.held} held` });\n```\n\nA held line keeps the cells the sheet states for it, found by the `line` the step answered, in\nfields of its own. A held row\'s `key` and `values` name fields by their keys, as every record a step answers a body does, so\nthe body reads the sheet again rather than those names. It is applied by the act on it, whose workflow hands the step those values again and\nlets it write over what the plant holds. A cell the line holds none of is left out, so a held line\nnever empties a cell of the plant:\n\n```\nconst line = await get_record({ table_id: "sheet_lines", record_id: trigger.app_workflow.inputs.record_id });\nconst values = {\n code: line.data["code"],\n ...(isEmpty(line.data["name"]) ? {} : { name: line.data["name"] }),\n ...(isNull(line.data["price"]) ? {} : { price: line.data["price"] }),\n};\n\nconst applied = await import_rows({ // a workflow step\n table_id: "plants",\n match: ["code"],\n conflicts: { default: "overwrite" },\n rows: [{ values }],\n});\nif (applied.held > 0) {\n return({ status: "error", message: applied.held_rows[0].message });\n}\n\nawait update_records({ table_id: "sheet_lines", record_ids: [line.id], set: { outcome: "applied" } });\nreturn({ status: "success", message: "Applied." });\n```\n\nA sheet is put back by a member\'s agent on chat, MCP or the CLI: `undo_record_changes` over the plants its lines name,\n500 at a time, with the run the sheet kept.\n\nA shop matches a bank statement to the notes it is owed by an act that states `review`. That act\'s workflow only\nproposes: its body calls steps that read and no other, and runs no agent \u2014 either is refused when the body is saved \u2014\nand returns in `data.review` one changeset of the act\'s table, which the platform writes the approved rows of. Called\nwith what the act asks, it answers the proposal and nothing is written; called again with\n`approved: { updates: [{ record_id, sha }], creates: [{ key, sha }] }`, the body runs again and the platform writes\nthose rows as this second run proposes them, so the body never reads `approved`. The platform adds a `sha` to every row\nof the proposal it answers \u2014 the body states none, and one it states is dropped \u2014 and the caller echoes the `sha` of\neach row it approves. A row the second run proposes with other values than the `sha` was read from is left unwritten\n(`changed`), so the body must propose the same for the same inputs and the rows it reads.\nAn update is of a record the act is run over (`record_id`, or one of `record_ids`): one of any other record is left\nunwritten. An act over every row in view is handed no rows: its body reads the rows it may change, and any of them may\nbe proposed. A create carries a `key`, a text of the body\'s own that names the same line on every run\n(`key: toString(line["Reference"])`), unique among the creates; where a confirm repeated must not add a second row,\nthe body also writes the key into a field the table keeps unique, so the repeat\'s create is left as a `conflict`.\nEach row\'s `data` holds only stored fields that change \u2014 never one the platform computes or stamps, nor a file \u2014 with\na `note` and a `ticked` the reader starts from, and `unplaced` lists what was read and matched nothing. Every approved\nrow is checked against its fields before any is written, and they are written in one transaction, so a flawed one\nwrites none. The status history of every row written that moved, or was made holding a status, is recorded in the\nsame run. The answer\'s `applied.previous` holds what each updated record\'s written fields held before, so one `update_records` of it puts the values back, and `delete_records` of a `created` record takes that row away. A body that stops proposing a row once it is\nwritten \u2014 it matches against what the rows hold now \u2014 makes approving twice write once.\n\n```\nconst inputs = trigger.app_workflow.inputs;\n\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: inputs.statement,\n columns: ["Reference", "Amount", "Paid on"],\n});\nconst unpaid = await query_records({\n table_id: "notes",\n filters: { field_key: "paid_on", operator: "is_empty" },\n pagination: { offset: 0, limit: 500 },\n});\nconst acted = groupBy(inputs.record_ids, (id) => toString(id));\nconst owed = filter(unpaid.records, (one) => size(acted[toString(one.id)] ?? []) > 0);\nconst byRef = groupBy(owed, (one) => toString(one.data["ref"]));\n\nconst lines = sheet.rows.map((line) => ({\n line,\n note: first(byRef[toString(line["Reference"])] ?? []),\n}));\nconst updates = filter(lines, (one) => !isNull(one.note)).map((one) => ({\n record_id: toString(one.note?.id),\n note: `Transfer of ${toString(one.line["Amount"])}`,\n ticked: toNumber(one.line["Amount"]) == sumBy(filter(owed, (owing) => owing.id == one.note?.id), (owing) => coalesce(owing.data["amount"], 0)),\n data: { paid_on: toString(one.line["Paid on"]) },\n}));\nconst unplaced = filter(lines, (one) => isNull(one.note)).map((one) => ({\n title: `${toString(one.line["Reference"])}: ${toString(one.line["Amount"])}`,\n note: "Matches no unpaid note",\n}));\n\nreturn({\n status: "success",\n message: `${size(updates)} of ${size(lines)} transfers match a note`,\n data: { review: { version: 1, table_id: "notes", creates: [], updates, unplaced } },\n});\n```\n\n### Saving a record with its lines\n\n`save_record`, a workflow step, saves one record and the rows of other tables that hang off it, all or\nnothing, and recognises what exists by the identity the input carries \u2014 so a call sent twice updates and\nadds nothing. `match` is the record\'s identity, `{ field_key: value }`, and must be exactly a uniqueness\nits table declares: one field marked `unique`, or the fields of one of the table\'s `unique` sets together\n(text, number, date, single select and one-row link fields). A `match` that is no declared uniqueness is\nrefused, naming what the table does declare. A live record carrying the match is found and only the cells\nof `set` that differ are written; none is found and it is added with `match` and `set`. `add_to` appends\nto a files, multi-select or many-link field and keeps what it holds.\n\n`lines` saves rows of other tables that link the record. Each entry names the child `table_id`, its\n`link` \u2014 a field of the child holding one row of this table, which the step fills and no row states \u2014\nthe child fields that, with `link`, are one uniqueness of the child table (`match`), and `rows`, each\nstating every `match` field. A line found by its match changes only the cells that differ, one not found\nis added, and a line the call does not list is left as it is: the step deletes nothing. Two rows of one\nentry cannot share a match.\n\nEverything is checked before the first write, and everything is written in one transaction: a table check\nthat refuses one line, a locked record or line the call would change, a value a field does not take, or a\ntable the run may not write refuses the call and saves nothing, naming the row. Two calls of one identity\ntake turns, so the second finds what the first saved. The answer is `record_id`, `created`,\n`updated_field_keys` (the cells changed on a record that existed) and, per entry of `lines`, the ids it\nadded, changed and left unchanged; these ids are typed for their tables, so a declared `record_link`\noutput or a follow-up write links straight to them.\n\n```\nconst inputs = trigger.app_workflow.inputs;\n\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: inputs.release_order,\n columns: ["Size", "Quantity"],\n});\n\nconst saved = await save_record({ // a workflow step\n table_id: "release_orders",\n match: { number: inputs.number },\n set: { note: "Read from the paper" },\n lines: [{\n table_id: "release_lines",\n link: "order",\n match: ["size"],\n rows: sheet.rows.map((row) => ({\n size: toString(row["Size"]),\n quantity: toNumber(row["Quantity"]),\n })),\n }],\n});\n\nreturn({\n status: "success",\n message: `${size(saved.lines[0].created_ids)} lines added`,\n data: { record_id: saved.record_id },\n});\n```\n\n## Reaching a connected service\n\nA service is reached only by a workflow step, and a step is called inside an app\'s workflow or an automation. From\nnothing to an app\'s run:\n\n1. `get_workspace` with `kind: "connection"` lists each `connected_account_id`.\n2. `set_app` declares a Workflow node that states what a call takes and hands back:\n\n```json\n{\n "type": "Workflow",\n "key": "invoice_order",\n "description": "Issues the KiotViet invoice for a confirmed order and hands back its code.",\n "inputs": { "order": { "type": "record_link", "entity": "order" } },\n "outputs": { "invoice_code": { "type": "text" } }\n}\n```\n\n3. `set_app_workflow` with the app, the alias `invoice_order` and this `source` (the step\'s entry is in `workflow_steps`):\n\n```\nconst order = await get_record({ table_id: "orders", record_id: trigger.app_workflow.inputs.order });\nconst made = await kiotviet_create_invoice({ // a workflow step\n connected_account_id: "cac_\u2026",\n branch_id: 1,\n sold_by_id: 7,\n lines: [{ product_id: 42, quantity: toNumber(order.data["quantity"]), price: toNumber(order.data["price"]) }],\n});\nreturn({ status: "success", message: "Invoiced", data: { invoice_code: made.invoice.code } });\n```\n\n4. `run_app_workflow` with `dry_run: true`, then the same call without it.\n\nA step wants the provider\'s own ids (`branch_id`, `sold_by_id`, `product_id`) and no tool lists them: declare a Workflow\nwhose body calls the provider\'s `list_*` workflow step (`kiotviet_list_branches`), run it, and read the ids from its answer.\nNo step lists KiotViet\'s users: read `sold_by_id` off a row `kiotviet_list_invoices` returns (KiotViet\'s `soldById`), or ask the owner.\n\n## Table checks\n\nA table check is one condition its table\'s record writes must not meet, declared beside the table\'s\n`unique`. It is not a workflow: it reads the write and a few lookups and answers yes or no, so it\ncannot change data. It runs inside the write, on every create, update or delete it is `on`, whichever\nsurface makes it \u2014 a person, the API, the CLI, an agent, a workflow. A refusal writes nothing and\nanswers with the check\'s message, under its field when the check names one, and names the check.\n\nA check is stated in its table\'s document, under `checks`, each under an alias of its own, and\n`set_tables` writes it with the rest of the table: a merge patch edits one, `null` removes it, and a\ncall that makes a table can state its checks, a lookup naming another table of the call by its `ref`.\n`get_table` states a table\'s checks in its document.\n\n| part | what it holds |\n|---|---|\n| `on` | the writes it checks: any of `create`, `update`, `delete` |\n| `when` | optional expression; the check runs only on a write where it is truthy. It cannot read lookups. |\n| `lookups` | optional, by name, the rows to read: `{ table, filter, limit }`, `table` an id or the `ref` of a table of the call, the filter as `query_records` takes it with fields and select options named by alias (`id` matches a row by its own record id), `limit` from 1 to 50 (50 when left out). A filter value that reads the record is `{ "$expr": "record.code" }`. |\n| `fail_when` | expression; truthy refuses the write |\n| `message` | expression for the refusal\'s text \u2014 a fixed text is a quoted string |\n| `field` | optional; the alias of the field the refusal is shown under |\n| `enabled` | `false` keeps the check without deciding writes; absent, it decides |\n\nEvery expression is written in the grammar above, names a table\'s fields and select options by\nalias (`record.status == "closed"`), and reads:\n\n- `record` \u2014 the record as it will be stored; on a delete, the record being deleted.\n- `prev_record` \u2014 on an update or a delete, the record before.\n- `changes` \u2014 on an update, the fields it changes. `!isNull(changes.photos)` asks whether a field changed.\n- `lookups.<name>` \u2014 in `fail_when` and `message`, the rows that lookup read. A row reads as `record` does: its fields by alias beside `id` (`row.start_on`), and a lookup field as the flat list of what it looks up (`first(row.booking_start)` through a one-row link). A lookup over the written table never returns the row being checked, and finds its other rows as the write leaves them, this call\'s other rows included. A field given to a helper as text names it by alias too (`sumBy(lookups.paid, "amount")`); text that names no field of the rows\' table is refused when the check is saved.\n- `runtime.timezone`, `runtime.workspace_id`, `runtime.organization_id`, `runtime.change_origin` (the\n write\'s origin), `runtime.triggered_by_member_id` (the member writing; null for a write no member\n made), `now()`, and `member_of("grp_\u2026")` \u2014 true when the member writing belongs to the group.\n\nIn a check a select reads as its one value (`record.kind == "retail"`), and a link (a one-row link too) and a lookup read as lists (`first(record.customer)`).\n\nA check on several operations reads only what all of them carry, so one on creates and updates reads\nneither `prev_record` nor `changes`.\n\nA write that recomputes another table\'s rows (a rollup over lines, a lookup or a formula over a parent) judges each recomputed row as an update, by the checks `on` `update` that read a field the recompute changed; `changes` is what the recompute changed. A check that refuses on `changes` or on a status the row holds can therefore refuse the write that moved the parent, so it is gated on the field it means (`!isNull(changes.status)`).\n\nA lookup reads with the access of the member who last saved the check, so saving one requires reading\nevery table its lookups read. It reads committed rows: two writes at the same moment can each pass a\ncheck the other would fail, so values no two rows may share are the table\'s `unique`, not a check.\n\nA `when`, a lookup or a `fail_when` that cannot decide \u2014 it fails, or the write\'s time runs out \u2014\nrefuses the write, naming the part it was reading and, for an expression or a filter, what it met\n(`lookups.clash: Filter on "start_on": date \'before\' value \u2026 is not a date`). A draft is checked when it is submitted, as a create; its edits before that are\nnot. A restored record is checked as a create. Cloning a table copies its checks to the member who\nclones it, and is refused when that member cannot read a table a check\'s lookup reads.\n\nOne open order per reference, a row never counting against itself:\n\n```json\n{\n "tables": [\n {\n "ref": "orders",\n "label": "Orders",\n "fields": {\n "reference": { "type": "text", "label": "Reference" },\n "closed_at": { "type": "date", "label": "Closed at" }\n },\n "checks": {\n "one_open_order_per_reference": {\n "on": ["create", "update"],\n "when": "isNull(record.closed_at)",\n "lookups": {\n "same_ref": {\n "table": "orders",\n "filter": {\n "node_type": "group",\n "logic": "and",\n "children": [\n { "node_type": "condition", "field_key": "reference", "operator": "equals", "value": { "$expr": "record.reference" } },\n { "node_type": "condition", "field_key": "closed_at", "operator": "is_empty" }\n ]\n },\n "limit": 2\n }\n },\n "fail_when": "some(lookups.same_ref, (r) => r.id != record.id)",\n "message": "\\"An open order already has this reference.\\"",\n "field": "reference"\n }\n }\n }\n ]\n}\n```\n\n## Table workflows\n\nA table workflow is what follows every committed write of a table\'s rows, whoever makes it: the grid, an import, an agent or\nany app. An effect that must hold for all of them belongs to the table. A button on one screen is that app\'s act, a value fixed\nwhen a row is made is `default_from`, and a rule that refuses a write is a check; a table workflow runs after the write has\ncommitted and cannot refuse it.\n\nIt is stated in the table\'s document, under `workflows`, each under an alias of its own, and `set_tables` writes it with the\nrest of the table: a merge patch edits one, `null` removes it, and `get_table` states a table\'s workflows in its document, with\nthe failures of the last 7 days beside them, to whoever manages the table. A dry run of `set_tables` with a `record_id` says what\neach workflow the call adds or changes would write on that row; reads are real and nothing is written.\n\n| part | what it holds |\n|---|---|\n| `on` | the write it follows: `create`, `update` or `delete` |\n| `when` | optional expression over `record`, `prev_record` and `changes`; the workflow runs only after a write where it is truthy |\n| `changed` | optional, with `on: update`: field aliases; the workflow runs only after an update that changes any one of them |\n| `body` | the steps, written as an app workflow\'s body is |\n| `enabled` | `false` keeps the workflow without running it; absent, it runs |\n\n```json\n{\n "tables": [\n {\n "ref": "repairs",\n "label": "Repairs",\n "fields": {\n "status": { "type": "select", "label": "Status", "options": [{ "alias": "queued", "label": "Queued" }, { "alias": "fixed", "label": "Fixed" }] },\n "fixed_on": { "type": "date", "label": "Fixed on" }\n },\n "workflows": {\n "stamp_fixed": {\n "on": "update",\n "changed": ["status"],\n "when": "record.status == \\"fixed\\"",\n "body": "await update_records({ table_id: \\"repairs\\", record_ids: [trigger.record_id], set: { fixed_on: formatDate(now(), \\"yyyy-MM-dd\\") } });"\n }\n }\n }\n ]\n}\n```\n\nThe body names the table\'s fields, select options and other tables by alias, and reads the write as `record`,\n`prev_record` and `changes` and the row\'s id as `trigger.record_id`. It operates on records and integrations: a body\nthat calls a tool changing a table, an app or any other structure is refused when it is saved. It runs as the table\'s owner, so\nit reaches the tables the owner reaches, and its writes meet the table\'s checks like any other write; a write refused by a\ncheck fails that run and leaves the row as it was. A body names a field by alias where it can tell which table\'s field it is: a\nread of `record`, `prev_record` or `changes`, a row from `get_record` or `query_records` called with a literal `table_id`, and a\nloop over such rows. A name it cannot tie to a table, and an option compared to a value of unknown type (not a read of a field, and\nnot a text, number or other value the type proves holds no option), are refused with what to change. A link reads as the list of the row ids it holds, a one-row link too: compare `first(record.customer) == id`. A lookup reads as one flat list of what it looks up: `record.x != prev_record.x` on one is refused, so compare `first(record.x)` with `first(prev_record.x)`. A single member, a single select, a number or a text reads as the value itself: `first(record.technician)` is refused at save, naming the read.\n\nA write the workflow itself makes sets off the workflows of its table in turn, and a chain of them stops after five. A workflow\nthat writes the row it follows is gated by a `changed` or a `when` that its own write does not satisfy. One run is queued for a\nrow and a workflow at a time, so a burst of writes to one row runs it once more after the run in flight. A field a workflow\nreads cannot be deleted or retyped until the workflow is changed or removed in the same call or before. Cloning a table copies its\nworkflows for whoever manages it (`include_workflows: false` leaves them), each with a body of its own, and is refused when a\nbody names a table the cloner cannot reach. A workflow runs as the table\'s owner, so whoever saves or rehearses one must manage the\ntable and read every table it reads in full: the body\'s, the gate\'s links\', the written table. An option is written where\nit is compared to, listed against or set on its field; a constant holding one is refused.\n\nA lock is the table\'s: a table workflow calls `lock_records` on a row (an app\'s act may too), and every door then refuses\nthe row\'s writes until whoever manages the table calls `unlock_records` \u2014 in an app\'s run, judged as the member who pressed.\nA locked row reads locked in every app. An app\'s own Check or `edit: false` only makes a row read-only in that app.\n\n## Runs\n\nEach start of a workflow is a run: an app\'s workflow or act, a table workflow, a button, an automation. An agent finds,\nreads, retries and stops runs on chat, MCP and the CLI. A run is read by whoever manages what its workflow belongs to: its\nautomation or its table, and for an app\'s workflow the app\'s owner or an admin, never a manager of the app.\n\n- **Find.** `query_workflow_executions` lists one workflow\'s runs (`workflow_id`), every run started for one row\n (`record_id`) \u2014 a table workflow\'s written row, a pressed button\'s row, an app run\'s `record_id` input \u2014 or both, newest\n first, a page at a time (`next_cursor`). Each run names its workflow and kind, what started it, its row and how it ended:\n for a failure, the step that threw, its tool and the error; for a waiting run, when it wakes, or the approval and the\n approvers it waits on, or the payment. A run started before runs recorded their row carries no `record_id`.\n- **Read.** `get_workflow_execution` gives one run\'s steps in order with each one\'s tool, input, output and error, what it\n returned, how many rows it wrote (a row\'s own history is in `query_audit_logs`) and the runs its writes started. Ninety days\n after a run starts, its trigger and its steps\' input and output are pruned; the read says so and gives the rest.\n `run_app_workflow` answers each run it makes with its `execution_id`.\n- **Retry.** `retry_workflow_execution` runs a failed, refused or cancelled run again as a new run linked to the first\n (`retry_of`): the first run\'s trigger and row, the workflow\'s current steps and its owner\'s current access, with the\n member who retries it as the one who started it. A run is retried once; a retry that fails is retried in turn. It asks\n first when the run, or a run it retried, already wrote rows or called a service no delete reverses. An automation\'s or a\n table workflow\'s retry runs in the background, as its runs do, and answers `queued`; a button\'s and an app\'s run while\n the call waits. An app\'s call is checked again against the workflow\'s current inputs. A pruned run, a run of a workflow\n that is off, a reviewed act\'s run (call the act again) and a recording\'s filing (the recording retries its delivery)\n are not run again.\n- **Stop.** `cancel_workflow_execution` stops a waiting run and withdraws its pending approvals.\n\nOff means off: a waiting run whose workflow was turned off, deleted, or taken off its automation, table, button or app ends\n`cancelled` when it would resume, saying why, and runs no further step.\n\nA waiting run keeps its version: when it resumes, it runs the steps of the version it started with, so an edit saved while it\nwaited changes later runs only. When that version is gone, the run ends failed and says why.\n';
1793
1793
 
1794
1794
  // docs/workflow_steps.md
1795
1795
  var workflow_steps_default = '<!-- Generated by backend/scripts/generate_workflow_steps_doc.ts from each step\'s schemas \u2014 never edit by hand. -->\n\n# Workflow steps\n\nThe steps only a workflow body runs: a connection\'s (an invoice issuer, a mailbox, \u2026) and the platform\'s own. Each is\ncalled as `await <step>({ ...inputs })` (see `workflows`); a step\'s inputs and answer are its entry here. A connection\nstep takes the `connected_account_id` that `get_workspace` lists under kind `connection`, and the app\'s owner and\nwhoever saves the body must both be able to use that connection.\n\n**How a step fails.** A step that is refused \u2014 an input the schema rejects, a connection the workspace lacks, an\nanswer the provider refuses \u2014 ends the run, and the run\'s error reads `[<step>] <reason>`. Inside `try { \u2026 } catch\n(e) { \u2026 }` the body handles it instead: `e` is `{ message, type, step_id?, detail? }`, `message` the reason as\nabove. A provider\'s refusal carries the provider\'s own words.\n\n## bkav\n\n### bkav_attach_files (workflow step)\n\n- Attach workspace files (a bi\xEAn b\u1EA3n, a b\u1EA3ng k\xEA) to an invoice on Bkav.\n- Bkav accepts PDF and Excel, at most 1 MB each.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | | |\n| `partner_invoice_id` | string | | Your PartnerInvoiceStringID, when invoice_guid is not set |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_cancel_invoices (workflow step)\n\n- Cancel issued invoices.\n- A cancelled invoice still reports to the tax authority; correcting one instead is a replacement or adjustment through bkav_submit_invoices. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | | InvoiceGUIDs Bkav returned |\n| `partner_invoice_ids` | string[] | | Your PartnerInvoiceStringIDs |\n| `reason` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_create_bill (workflow step)\n\n- Record a cash-register bill on Bkav.\n- Field names are Bkav\'s.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `bill` | object | yes | |\n| `bill.Bill` | object | yes | |\n| `bill.Bill.BillCode` | number | yes | |\n| `bill.Bill.OriginalBillCode` | number | | |\n| `bill.Bill.BillDate` | string | yes | |\n| `bill.Bill.BillStatusID` | number | yes | |\n| `bill.Bill.PayMethod` | string | | |\n| `bill.Bill.Note` | string | | |\n| `bill.ListBillDetails` | object[] | yes | |\n| `bill.ListBillDetails[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `bill.ListBillDetails[].IsDiscount` | boolean | | |\n| `bill.ListBillDetails[].ItemCode` | string | | |\n| `bill.ListBillDetails[].ItemName` | string | yes | |\n| `bill.ListBillDetails[].UnitName` | string | | |\n| `bill.ListBillDetails[].Qty` | number | | |\n| `bill.ListBillDetails[].Price` | number | | |\n| `bill.ListBillDetails[].Amount` | number | | |\n| `bill.ListBillDetails[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `bill.ListBillDetails[].TaxRate` | number | | |\n| `bill.ListBillDetails[].TaxAmount` | number | | |\n| `bill.ListBillDetails[].OtherAmount` | number | | |\n| `bill.ListBillDetails[].DiscountRate` | number | | |\n| `bill.ListBillDetails[].DiscountAmount` | number | | |\n| `bill.ListBillDetails[].UserDefineDetails` | string | | |\n| `bill.ListBillDetails[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `bill.ListBillDetails[].SpecialtyItems` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `created` | true | yes | |\n\n### bkav_create_invoice_from_bills (workflow step)\n\n- Raise one invoice from cash-register bills, by their BillGUIDs.\n- Field names are Bkav\'s.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `input` | object | yes | |\n| `input.Invoice` | object | yes | |\n| `input.Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `input.Invoice.InvoiceDate` | string | yes | ISO date |\n| `input.Invoice.BuyerCode` | string | | |\n| `input.Invoice.BuyerName` | string | | |\n| `input.Invoice.BuyerTaxCode` | string | | |\n| `input.Invoice.BuyerUnitName` | string | | |\n| `input.Invoice.BuyerAddress` | string | | |\n| `input.Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `input.Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `input.Invoice.ReceiverEmail` | string | | |\n| `input.Invoice.ReceiverMobile` | string | | |\n| `input.Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.Note` | string | | |\n| `input.Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.CurrencyID` | string | | |\n| `input.Invoice.ExchangeRate` | number | | |\n| `input.Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `input.Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `input.Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `input.Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `input.Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `input.Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `input.Invoice.isBTH` | boolean | | |\n| `input.Invoice.CCCD` | string | | |\n| `input.Invoice.PassportNumber` | string | | |\n| `input.Invoice.FiscalCodes` | string | | |\n| `input.Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `input.Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `input.Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `input.BillGUIDs` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_delete_invoices (workflow step)\n\n- Delete invoices that were never issued.\n- A numbered one can be deleted only if its number is the highest in its range.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | | InvoiceGUIDs Bkav returned |\n| `partner_invoice_ids` | string[] | | Your PartnerInvoiceStringIDs |\n| `reason` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_explain_invoice_error (workflow step)\n\n- Explain an erroneous invoice to the tax authority without replacing or adjusting it, optionally answering one of its notices.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `reason` | string | yes | |\n| `notice_number` | string | | S\u1ED1 of the authority\'s notice being answered |\n| `notice_date` | string | | yyyy-mm-dd; with notice_number |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise Msg says why |\n| `results[].Msg` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_explain_replaced_invoice (workflow step)\n\n- Explain to the tax authority an invoice that was replaced or adjusted.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `reason` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise Msg says why |\n| `results[].Msg` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_get_bill (workflow step)\n\n- Read a cash-register bill.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `seller_tax_code` | string | yes | |\n| `organization_code` | string | yes | |\n| `bill_code` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `bill` | object | yes | |\n\n### bkav_get_error_notice_pdf (workflow step)\n\n- Save the PDF of an invoice\'s 04/SS error notice as a workspace file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_file_by_lookup_code (workflow step)\n\n- Save an invoice\'s display copy, conversion copy or XML as a workspace file, found by the m\xE3 tra c\u1EE9u the buyer was sent.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `lookup_code` | string | yes | M\xE3 tra c\u1EE9u |\n| `kind` | "display" \\\\| "converted" \\\\| "xml" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_invoice (workflow step)\n\n- Read one invoice as Bkav holds it \u2014 header, lines, status and m\xE3 tra c\u1EE9u.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `id` | string | yes | Your PartnerInvoiceStringID or Bkav\'s InvoiceGUID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### bkav_get_invoice_file (workflow step)\n\n- Save an invoice\'s PDF or signed XML as a workspace file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n| `format` | "pdf" \\\\| "xml" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_invoice_history (workflow step)\n\n- Read everything that happened to an invoice on Bkav, oldest first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `entries` | object[] | yes | |\n\n### bkav_get_invoice_link (workflow step)\n\n- Get a download link for an invoice\'s display copy, conversion copy, or its 04/SS error notice.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n| `kind` | "display" \\\\| "converted" \\\\| "notice_04ss" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `url` | string | yes | |\n\n### bkav_get_invoice_status (workflow step)\n\n- Read an invoice\'s status on Bkav.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice_status_id` | number | yes | 1 m\u1EDBi t\u1EA1o, 2 \u0111\xE3 ph\xE1t h\xE0nh, 3 \u0111\xE3 hu\u1EF7, 5\u20138 ch\u1EDD/\u0111\xE3 thay th\u1EBF\u2013\u0111i\u1EC1u ch\u1EC9nh, 11 \u0111\xE3 c\u1EA5p s\u1ED1 ch\u1EDD k\xFD |\n\n### bkav_get_listing (workflow step)\n\n- Save the b\u1EA3ng k\xEA of one invoice type over a window as a workspace spreadsheet.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_type_id` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, \u2026 |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_report_bc26 (workflow step)\n\n- Save the BC26/AC invoice usage report for a window as a workspace XML file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_tax_status (workflow step)\n\n- Read the tax authority\'s status for an invoice and its m\xE3 c\u1EE7a CQT once granted.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `statuses` | object[] | yes | |\n| `statuses[].BkavStatus` | number | yes | InvoiceStatusID: 1 m\u1EDBi t\u1EA1o, 2 \u0111\xE3 ph\xE1t h\xE0nh, 3 \u0111\xE3 hu\u1EF7, \u2026 |\n| `statuses[].TaxStatus` | number | yes | 32 waiting, 33 accepted, 34\u201339 returned or failed |\n| `statuses[].TaxAuthorityCode` | string \\\\| null | yes | M\xE3 c\u1EE7a C\u01A1 quan thu\u1EBF, once granted |\n| `statuses[].ErrorContent` | string \\\\| null | yes | |\n\n### bkav_list_invoices_by_date (workflow step)\n\n- List invoices dated within a window, one page at a time.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n| `page` | number | | 1-based |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoices` | object[] | yes | |\n\n### bkav_list_invoices_by_number (workflow step)\n\n- List invoices of one m\u1EABu s\u1ED1 and k\xFD hi\u1EC7u by number range.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `form` | string | yes | M\u1EABu s\u1ED1 |\n| `serial` | string | yes | K\xFD hi\u1EC7u |\n| `from_number` | number | yes | |\n| `to_number` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoices` | object[] | yes | |\n\n### bkav_lookup_company (workflow step)\n\n- Look up a company in the tax registry by M\xE3 s\u1ED1 thu\u1EBF, through the Bkav account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `tax_code` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `company` | object | yes | |\n| `company.MaSoThue` | string | yes | |\n| `company.TenChinhThuc` | string | yes | |\n| `company.DiaChiGiaoDichChinh` | string | yes | |\n| `company.DiaChiGiaoDichPhu` | string | yes | |\n| `company.TrangThaiHoatDong` | string | yes | |\n| `company.ChuDoanhNghiep` | string | | |\n| `company.SoDienThoai` | string \\\\| null | | |\n| `company.LastUpdate` | string | | |\n\n### bkav_send_lookup_email (workflow step)\n\n- Send an invoice\'s lookup email to the buyer again, or to another email or phone.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `email` | string | | |\n| `mobile` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `sent` | true | yes | |\n\n### bkav_sign_invoices (workflow step)\n\n- Sign \u2014 issue \u2014 invoices with the account\'s HSM signature, making them tax-registered documents.\n- An account that signs with a USB token cannot sign this way.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `signed` | string[] | yes | |\n\n### bkav_submit_invoices (workflow step)\n\n- Create, replace or adjust invoices on Bkav, in Bkav\'s field names.\n- Each carries PartnerInvoiceStringID, your own id for it, which Bkav uses to refuse a duplicate.\n- Attachments are not accepted here.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `kind` | "create_draft" \\\\| "create" \\\\| "create_draft_own_serial" \\\\| "create_own_number" \\\\| "create_own_serial" \\\\| "replace_draft" \\\\| "adjust_draft" \\\\| "adjust_discount_draft" \\\\| "replace" \\\\| "adjust" \\\\| "adjust_external_draft" \\\\| "adjust_discount" \\\\| "adjust_external" \\\\| "replace_external" \\\\| "replace_external_draft" | yes | create_draft: no number, deletable. create: Bkav numbers it. *_own_*: your form/serial. replace_* / adjust_*: each invoice names its original in Invoice.OriginalInvoiceIdentify and gives Invoice.Reason. *_external*: corrects an invoice issued outside Bkav; its first line is an ItemTypeID 4 description of it. *_draft kinds stay unnumbered. |\n| `invoices` | object[] | yes | |\n| `invoices[].Invoice` | object | yes | |\n| `invoices[].Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `invoices[].Invoice.InvoiceDate` | string | yes | ISO date |\n| `invoices[].Invoice.BuyerCode` | string | | |\n| `invoices[].Invoice.BuyerName` | string | | |\n| `invoices[].Invoice.BuyerTaxCode` | string | | |\n| `invoices[].Invoice.BuyerUnitName` | string | | |\n| `invoices[].Invoice.BuyerAddress` | string | | |\n| `invoices[].Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `invoices[].Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `invoices[].Invoice.ReceiverEmail` | string | | |\n| `invoices[].Invoice.ReceiverMobile` | string | | |\n| `invoices[].Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.Note` | string | | |\n| `invoices[].Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.CurrencyID` | string | | |\n| `invoices[].Invoice.ExchangeRate` | number | | |\n| `invoices[].Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `invoices[].Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `invoices[].Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `invoices[].Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `invoices[].Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `invoices[].Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `invoices[].Invoice.isBTH` | boolean | | |\n| `invoices[].Invoice.CCCD` | string | | |\n| `invoices[].Invoice.PassportNumber` | string | | |\n| `invoices[].Invoice.FiscalCodes` | string | | |\n| `invoices[].Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `invoices[].Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `invoices[].Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `invoices[].ListInvoiceDetailsWS` | object[] | yes | |\n| `invoices[].ListInvoiceDetailsWS[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].IsDiscount` | boolean | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemCode` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemName` | string | yes | |\n| `invoices[].ListInvoiceDetailsWS[].UnitName` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].Qty` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Price` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Amount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].TaxRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].OtherAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].UserDefineDetails` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `invoices[].ListInvoiceDetailsWS[].SpecialtyItems` | string | | |\n| `invoices[].PartnerInvoiceStringID` | string | yes | |\n| `invoices[].AttachListNumber` | string | | S\u1ED1 of a b\u1EA3ng k\xEA the invoice refers to |\n| `invoices[].AttachListDate` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_update_invoices (workflow step)\n\n- Change invoices not yet issued. by names how each invoice is found: partner (its partner id), number (Invoice.InvoiceForm + InvoiceSerial + InvoiceNo) or guid (Invoice.InvoiceGUID).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `by` | "partner" \\\\| "number" \\\\| "guid" | yes | |\n| `invoices` | object[] | yes | |\n| `invoices[].Invoice` | object | yes | |\n| `invoices[].Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `invoices[].Invoice.InvoiceDate` | string | yes | ISO date |\n| `invoices[].Invoice.BuyerCode` | string | | |\n| `invoices[].Invoice.BuyerName` | string | | |\n| `invoices[].Invoice.BuyerTaxCode` | string | | |\n| `invoices[].Invoice.BuyerUnitName` | string | | |\n| `invoices[].Invoice.BuyerAddress` | string | | |\n| `invoices[].Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `invoices[].Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `invoices[].Invoice.ReceiverEmail` | string | | |\n| `invoices[].Invoice.ReceiverMobile` | string | | |\n| `invoices[].Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.Note` | string | | |\n| `invoices[].Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.CurrencyID` | string | | |\n| `invoices[].Invoice.ExchangeRate` | number | | |\n| `invoices[].Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `invoices[].Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `invoices[].Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `invoices[].Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `invoices[].Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `invoices[].Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `invoices[].Invoice.isBTH` | boolean | | |\n| `invoices[].Invoice.CCCD` | string | | |\n| `invoices[].Invoice.PassportNumber` | string | | |\n| `invoices[].Invoice.FiscalCodes` | string | | |\n| `invoices[].Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `invoices[].Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `invoices[].Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `invoices[].ListInvoiceDetailsWS` | object[] | yes | |\n| `invoices[].ListInvoiceDetailsWS[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].IsDiscount` | boolean | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemCode` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemName` | string | yes | |\n| `invoices[].ListInvoiceDetailsWS[].UnitName` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].Qty` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Price` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Amount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].TaxRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].OtherAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].UserDefineDetails` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `invoices[].ListInvoiceDetailsWS[].SpecialtyItems` | string | | |\n| `invoices[].PartnerInvoiceStringID` | string | yes | |\n| `invoices[].AttachListNumber` | string | | S\u1ED1 of a b\u1EA3ng k\xEA the invoice refers to |\n| `invoices[].AttachListDate` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n## drive\n\n### drive_export_file (workflow step)\n\n- Upload a Lotics file to the member\'s Google Drive.\n- The file is uploaded as-is \u2014 a .docx stays a .docx, a .xlsx stays a .xlsx.\n- It is deliberately NOT converted into a Google Doc or Sheet: conversion re-flows the layout of a generated document, and a native Google file is live, so its figures would drift away from the record they came from.\n- Use this to archive a finished document, the way you would attach it to an email.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `file_id` | string | yes | Lotics file ID to upload |\n| `folder_id` | string | | Drive folder ID to place the file in; omit for the member\'s Drive root |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `drive_file_id` | string | yes | Drive file ID of the uploaded copy |\n| `web_view_link` | string \\\\| null | yes | URL to open the uploaded file in Drive |\n\n### drive_import_file (workflow step)\n\n- Import a Google Drive file into the workspace as a Lotics file, and return its file id.\n- Google Docs arrive as .docx, Sheets as .xlsx, Slides as .pptx; everything else transfers unchanged. **This is also how you READ a Drive file.** There is no separate Drive read tool: import the file, then use `view_files` on the returned id. (chat)\n- That path handles scanned documents \u2014 a photographed B/L or a stamped invoice inside a .docx reaches the model as a picture.\n- Exporting Drive content as plain text would return an empty document for exactly those files.\n- Folders, Forms, shortcuts and other Drive items with no file form cannot be imported.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `file_id` | string | yes | Drive file ID, from drive_query_files (workflow step, chat) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | Lotics file ID \u2014 pass this to view_files to read the contents (chat) |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n### drive_query_files (workflow step)\n\n- Search the member\'s Google Drive using Drive query syntax. ## Query syntax Terms are `field operator value`; combine with `and` / `or` / `not`.\n- String values are single-quoted. - Name: `name = \'Rate card\'`, `name contains \'invoice\'` - Full text (body + title): `fullText contains \'CMA CGM\'` - Type: `mimeType = \'application/vnd.google-apps.spreadsheet\'`, `mimeType contains \'pdf\'` - Folder: `\'<folder_id>\' in parents` - Time (RFC 3339): `modifiedTime > \'2026-01-01T00:00:00\'` - Exclude deleted: `trashed = false` (always worth adding) - Starred / shared: `starred = true`, `sharedWithMe = true` Google-native types: `application/vnd.google-apps.document` (Docs), `.spreadsheet` (Sheets), `.presentation` (Slides), `.folder`.\n- Examples: - `name contains \'packing list\' and trashed = false` - `\'1a2b3c\' in parents and modifiedTime > \'2026-08-01T00:00:00\'` - `fullText contains \'demurrage\' and mimeType = \'application/vnd.google-apps.document\'` Results carry enough metadata (name, type, modified time, size, owner) to pick a file WITHOUT importing it.\n- Import only the one you actually need \u2014 `drive_import_file` persists a copy in the workspace. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `query` | string | yes | Drive query syntax expression |\n| `max_result` | number | yes | Maximum number of files to retrieve (1-50) |\n| `order_by` | string | | Sort order, e.g. \'modifiedTime desc\', \'name\' |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `files` | object[] | yes | List of matching Drive files |\n| `files[].id` | string | yes | Drive file ID \u2014 pass this to drive_import_file (workflow step, chat) |\n| `files[].name` | string | yes | File name as it appears in Drive |\n| `files[].mime_type` | string | yes | Drive MIME type; \'application/vnd.google-apps.*\' means Google-native |\n| `files[].modified_time` | string \\\\| null | yes | RFC 3339 timestamp of the last modification |\n| `files[].size_bytes` | number \\\\| null | yes | Size in bytes; null for Google-native files, which have no stored size until exported |\n| `files[].owner` | string \\\\| null | yes | Email address of the file\'s owner |\n| `files[].web_view_link` | string \\\\| null | yes | URL to open the file in Drive |\n\n## evaluate\n\n### evaluate_formula (workflow step)\n\n- Compute a formula over one record as its table\'s own formula fields compute: fields referenced as `{alias}`, lookups and rollups read as the row holds them.\n- Writes nothing.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `record_id` | string | yes | |\n| `expression` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `value` | number \\\\| string \\\\| boolean \\\\| null | yes | |\n\n## excel\n\n### excel_get_rows (workflow step)\n\n- Read every row of a sheet in an .xlsx, .xls or .csv file as a list of objects keyed by the header row\'s text.\n- A cell is its typed value: a number, true or false, text, a date as ISO text ("2026-03-02", "2026-03-02T09:30:00", or "09:30:00" for a time alone), null when blank.\n- A formula cell is the value the file last computed; an error cell is its code as text ("#N/A").\n- Every CSV cell is text or null.\n- A header cell inside a merge takes the merge\'s text; a blank header is keyed by its column letter ("C"), and a header text that repeats gets its column letter added ("Amount (F)"). headers lists the keys in sheet order.\n- Rows above header_row and rows blank in every column are left out.\n- One call reads a sheet of at most 10000 rows, its header counted, and returns at most 250000 cells (rows \xD7 kept columns); a sheet past either is refused with its row count, never cut short.\n- Read-only.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | | Sheet name, or 0-based index. |\n| `header_row` | number | | 1-based row holding the headers. |\n| `columns` | string[] | | Keys to keep, as headers lists them. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | |\n| `header_row` | number | yes | |\n| `headers` | string[] | yes | The key of each returned column, in sheet order |\n| `row_count` | number | yes | |\n| `rows` | object[] | yes | |\n\n## facebook\n\n### facebook_create_post (workflow step)\n\n- Publish a post to a Facebook Page feed.\n- Requires a Facebook connected account whose page allows content management.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | Facebook Page to post to \u2014 one of the connected account\'s managed pages. |\n| `message` | string | yes | |\n| `link` | string | | A post carries either a link or media. |\n| `media_file_ids` | string[] | | Photos, or one video on its own. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n### facebook_list_comments (workflow step)\n\n- Read the most recent comments left on a Facebook Page\'s posts.\n- Reply with facebook_reply_to_comment, passing the comment_id. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | One of the connected account\'s managed pages. |\n| `limit` | integer | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `comments` | object[] | yes | |\n| `comments[].comment_id` | string | yes | |\n| `comments[].post_id` | string | yes | |\n| `comments[].author_id` | string | yes | |\n| `comments[].author_name` | string | yes | |\n| `comments[].message` | string | yes | |\n| `comments[].created_time` | string | yes | |\n\n### facebook_list_conversations (workflow step)\n\n- Read a Facebook Page\'s Messenger conversations with their recent messages.\n- Answer someone with facebook_send_message, passing the person_id. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | One of the connected account\'s managed pages. |\n| `limit` | integer | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `conversations` | object[] | yes | |\n| `conversations[].conversation_id` | string | yes | |\n| `conversations[].person_id` | string \\\\| null | yes | |\n| `conversations[].person_name` | string \\\\| null | yes | |\n| `conversations[].messages` | object[] | yes | |\n| `conversations[].messages[].from_id` | string | yes | |\n| `conversations[].messages[].from_name` | string | yes | |\n| `conversations[].messages[].message` | string | yes | |\n| `conversations[].messages[].created_time` | string | yes | |\n\n### facebook_reply_to_comment (workflow step)\n\n- Reply publicly to a comment on a Facebook Page post, as the page.\n- The reply is visible to everyone who can see the comment.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | The page that owns the post the comment is on. |\n| `comment_id` | string | yes | From facebook_list_comments. (workflow step, chat) |\n| `message` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `comment_id` | string | yes | The reply\'s own id. |\n\n### facebook_send_message (workflow step)\n\n- Answer someone who messaged a Facebook Page, as the page.\n- Meta accepts a reply only within 24 hours of that person\'s last message.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | The page the person wrote to. |\n| `person_id` | string | yes | From facebook_list_conversations. Page-scoped to this page. (workflow step, chat) |\n| `message` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | |\n\n## fedex\n\n### fedex_cancel_draft (workflow step)\n\n- Cancel (delete) an unconfirmed FedEx draft (Open Ship) booking by its reference.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `reference` | string | yes | The draft\'s reference (Open Ship index) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `cancelled` | boolean | yes | |\n\n### fedex_confirm_shipment (workflow step)\n\n- Confirm a FedEx draft (Open Ship) booking by its reference, generating the label / AWB.\n- Returns the master tracking number and any piece tracking numbers + label URLs.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `reference` | string | yes | The draft\'s reference (Open Ship index) from fedex_create_draft_shipment (workflow step, chat) |\n| `master_tracking_number` | string | | The master_tracking_number returned by fedex_create_draft_shipment \u2014 required to finalize the label (workflow step, chat) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `master_tracking_number` | string | | |\n| `tracking_numbers` | string[] | yes | |\n| `label_urls` | string[] | yes | |\n\n### fedex_create_draft_shipment (workflow step)\n\n- Create a FedEx draft (Open Ship) booking \u2014 no label yet.\n- Confirm later with fedex_confirm_shipment. reference is a unique key you choose (e.g. the shipment order number) used to confirm or cancel. (workflow step, chat)\n- Supports multiple packages, special services, and (international) customs commodities.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `reference` | string | yes | Unique key for this draft (the Open Ship index) |\n| `shipper` | object | yes | |\n| `shipper.contact` | object | | |\n| `shipper.contact.person_name` | string | | |\n| `shipper.contact.company_name` | string | | |\n| `shipper.contact.phone_number` | string | | |\n| `shipper.address` | object | yes | |\n| `shipper.address.postal_code` | string | yes | |\n| `shipper.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.address.city` | string | | |\n| `shipper.address.state_or_province_code` | string | | |\n| `shipper.address.street_lines` | string[] | | |\n| `shipper.address.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.contact` | object | | |\n| `recipient.contact.person_name` | string | | |\n| `recipient.contact.company_name` | string | | |\n| `recipient.contact.phone_number` | string | | |\n| `recipient.address` | object | yes | |\n| `recipient.address.postal_code` | string | yes | |\n| `recipient.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.address.city` | string | | |\n| `recipient.address.state_or_province_code` | string | | |\n| `recipient.address.street_lines` | string[] | | |\n| `recipient.address.residential` | boolean | | |\n| `service_type` | string | yes | FedEx service type e.g. INTERNATIONAL_PRIORITY |\n| `packaging_type` | string | | FedEx packaging type e.g. YOUR_PACKAGING |\n| `packages` | object[] | yes | One entry per package; weight in kg |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `pickup_type` | string | | FedEx pickup type e.g. USE_SCHEDULED_PICKUP |\n| `special_services` | string[] | | FedEx special service types e.g. SATURDAY_DELIVERY, FEDEX_ONE_RATE |\n| `commodities` | object[] | | Customs commodities for international shipments |\n| `commodities[].description` | string | yes | |\n| `commodities[].quantity` | number | yes | |\n| `commodities[].customs_value` | number | yes | |\n| `commodities[].currency` | string | yes | |\n| `commodities[].weight` | number | yes | |\n| `commodities[].weight_units` | "KG" \\\\| "LB" | | |\n| `commodities[].country_of_manufacture` | string | | ISO 3166-1 alpha-2 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `reference` | string | yes | |\n| `status` | "draft" | yes | |\n| `master_tracking_number` | string | | Pass this to fedex_confirm_shipment to finalize the draft (workflow step, chat) |\n\n### fedex_create_shipment (workflow step)\n\n- Create a FedEx shipment and generate the label / AWB in one call.\n- Returns the master tracking number, piece tracking numbers, and label URLs.\n- Use the Open Ship draft flow instead when you need to stage a booking before committing.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `shipper` | object | yes | |\n| `shipper.contact` | object | | |\n| `shipper.contact.person_name` | string | | |\n| `shipper.contact.company_name` | string | | |\n| `shipper.contact.phone_number` | string | | |\n| `shipper.address` | object | yes | |\n| `shipper.address.postal_code` | string | yes | |\n| `shipper.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.address.city` | string | | |\n| `shipper.address.state_or_province_code` | string | | |\n| `shipper.address.street_lines` | string[] | | |\n| `shipper.address.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.contact` | object | | |\n| `recipient.contact.person_name` | string | | |\n| `recipient.contact.company_name` | string | | |\n| `recipient.contact.phone_number` | string | | |\n| `recipient.address` | object | yes | |\n| `recipient.address.postal_code` | string | yes | |\n| `recipient.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.address.city` | string | | |\n| `recipient.address.state_or_province_code` | string | | |\n| `recipient.address.street_lines` | string[] | | |\n| `recipient.address.residential` | boolean | | |\n| `service_type` | string | yes | FedEx service type e.g. INTERNATIONAL_PRIORITY |\n| `packaging_type` | string | | FedEx packaging type e.g. YOUR_PACKAGING |\n| `packages` | object[] | yes | One entry per package; weight in kg |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `pickup_type` | string | | FedEx pickup type e.g. USE_SCHEDULED_PICKUP |\n| `special_services` | string[] | | FedEx special service types |\n| `commodities` | object[] | | Customs commodities for international shipments |\n| `commodities[].description` | string | yes | |\n| `commodities[].quantity` | number | yes | |\n| `commodities[].customs_value` | number | yes | |\n| `commodities[].currency` | string | yes | |\n| `commodities[].weight` | number | yes | |\n| `commodities[].weight_units` | "KG" \\\\| "LB" | | |\n| `commodities[].country_of_manufacture` | string | | ISO 3166-1 alpha-2 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `master_tracking_number` | string | | |\n| `tracking_numbers` | string[] | yes | |\n| `label_urls` | string[] | yes | |\n\n### fedex_rate (workflow step)\n\n- Get FedEx rate quotes for a shipment.\n- Returns one priced row per available service (or just the requested service_type).\n- Use before booking to compare costs.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `shipper` | object | yes | |\n| `shipper.postal_code` | string | yes | |\n| `shipper.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.city` | string | | |\n| `shipper.state_or_province_code` | string | | |\n| `shipper.street_lines` | string[] | | |\n| `shipper.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.postal_code` | string | yes | |\n| `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.city` | string | | |\n| `recipient.state_or_province_code` | string | | |\n| `recipient.street_lines` | string[] | | |\n| `recipient.residential` | boolean | | |\n| `packages` | object[] | yes | One entry per package; weight in kg |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `service_type` | string | | FedEx service type e.g. INTERNATIONAL_PRIORITY. Omit to rate all available services. |\n| `packaging_type` | string | | FedEx packaging type e.g. YOUR_PACKAGING |\n| `pickup_type` | string | | FedEx pickup type e.g. USE_SCHEDULED_PICKUP |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `special_services` | string[] | | FedEx special service types e.g. SATURDAY_DELIVERY |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `services` | object[] | yes | |\n| `services[].service_type` | string | yes | |\n| `services[].service_name` | string | | |\n| `services[].total_charge` | number | | |\n| `services[].currency` | string | | |\n\n### fedex_service_availability (workflow step)\n\n- Check what FedEx services serve a lane. query "transit_times" returns delivery commitments per service; "package_and_service_options" returns valid service/packaging combinations; "special_service_options" returns available special services.\n- Use before rating/booking to learn the valid service types for an origin \u2192 destination.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `query` | "transit_times" \\\\| "special_service_options" \\\\| "package_and_service_options" | yes | |\n| `shipper` | object | yes | |\n| `shipper.postal_code` | string | yes | |\n| `shipper.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.city` | string | | |\n| `shipper.state_or_province_code` | string | | |\n| `shipper.street_lines` | string[] | | |\n| `shipper.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.postal_code` | string | yes | |\n| `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.city` | string | | |\n| `recipient.state_or_province_code` | string | | |\n| `recipient.street_lines` | string[] | | |\n| `recipient.residential` | boolean | | |\n| `packages` | object[] | yes | One entry per package; weight in kg unless weight_units set |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `carrier_codes` | string[] | yes | FedEx carrier codes \u2014 at least one, e.g. FDXE (Express), FDXG (Ground) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `result` | object | yes | |\n\n### fedex_validate_address (workflow step)\n\n- Validate one or more addresses with FedEx.\n- Returns each resolved address plus its classification (e.g.\n- BUSINESS / RESIDENTIAL) and deliverability attributes.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `addresses` | object[] | yes | |\n| `addresses[].postal_code` | string | yes | |\n| `addresses[].country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `addresses[].city` | string | | |\n| `addresses[].state_or_province_code` | string | | |\n| `addresses[].street_lines` | string[] | | |\n| `addresses[].residential` | boolean | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `resolved_addresses` | object[] | yes | |\n| `resolved_addresses[].city` | string | | |\n| `resolved_addresses[].state_or_province_code` | string | | |\n| `resolved_addresses[].postal_code` | string | | |\n| `resolved_addresses[].country_code` | string | | |\n| `resolved_addresses[].classification` | string | | |\n| `resolved_addresses[].attributes` | object | | |\n\n## generate\n\n### generate_bank_qr_code (workflow step)\n\n- Generate a bank-payment QR code (VietQR for Vietnam) and save it as a PNG to the workspace.\n- The QR encodes the recipient bank, account number, and optional amount/description so any banking app can scan and pre-fill the transfer.\n- Call list_banks first to discover valid bank codes. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `country` | "VN" | yes | Country whose bank-payment QR standard to use. Currently only \'VN\' (Vietnam / VietQR) is supported. |\n| `bank_code` | "ICB" \\\\| "VCB" \\\\| "BIDV" \\\\| "VBA" \\\\| "OCB" \\\\| "MB" \\\\| "TCB" \\\\| "ACB" \\\\| "VPB" \\\\| "TPB" \\\\| "STB" \\\\| "HDB" \\\\| "VCCB" \\\\| "SCB" \\\\| "VIB" \\\\| "SHB" \\\\| "EIB" \\\\| "MSB" \\\\| "CAKE" \\\\| "Ubank" \\\\| "TIMO" \\\\| "SGICB" \\\\| "BAB" \\\\| "momo" \\\\| "PVDB" \\\\| "PVCB" \\\\| "MBV" \\\\| "NCB" \\\\| "SHBVN" \\\\| "ABB" \\\\| "VAB" \\\\| "NAB" \\\\| "PGB" \\\\| "VIETBANK" \\\\| "BVB" \\\\| "SEAB" \\\\| "COOPBANK" \\\\| "LPB" \\\\| "KLB" \\\\| "KBank" \\\\| "CIMB" \\\\| "WVN" | yes | Bank code from list_banks. Must belong to the specified country. (workflow step, chat) |\n| `account_number` | string | yes | Beneficiary account number at the bank. |\n| `amount` | number | | Payment amount in the local currency (VND for Vietnam). Omit for a static QR with no fixed amount. |\n| `description` | string | | Transfer description / memo. Max 50 bytes; Vietnamese diacritics use 2\u20133 bytes per character. |\n| `account_name` | string | | Beneficiary name shown on the payer\'s banking app confirmation screen. Max 25 bytes \u2014 banks normalize Vietnamese names to ASCII uppercase, pass the normalized form (e.g. \'NGUYEN VAN A\'). |\n| `filename` | string | | Output filename. Defaults to \'bank_qr_<bank_code>.png\'. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n| `bank` | object | yes | |\n| `bank.country` | string | yes | |\n| `bank.code` | string | yes | |\n| `bank.short_name` | string | yes | |\n| `bank.full_name` | string | yes | |\n\n### generate_qr_code (workflow step)\n\n- Generate a QR code PNG from text or a URL and save it to the workspace.\n- For bank payment QR codes, use generate_bank_qr_code instead \u2014 it builds the correct EMVCo payload from a bank code and account number. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `data` | string | yes | Text or URL to encode into the QR code. |\n| `filename` | string | | Output filename. Defaults to \'qr_code.png\'. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n## gmail\n\n### gmail_get_thread (workflow step)\n\n- Retrieve all messages in a Gmail thread.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Gmail account to use |\n| `thread_id` | string | yes | Gmail thread ID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `messages` | object[] | yes | |\n| `messages[].id` | string | yes | Gmail message ID |\n| `messages[].threadId` | string | yes | Gmail thread ID |\n| `messages[].labelIds` | string[] | yes | Gmail label IDs (e.g. INBOX, UNREAD) |\n| `messages[].subject` | string | yes | Email subject |\n| `messages[].from` | string | yes | Sender email address |\n| `messages[].to` | string | yes | Recipient email address |\n| `messages[].date` | string | yes | Email date in the workspace timezone, with its offset |\n| `messages[].snippet` | string | yes | Short plain-text preview of the email body |\n| `messages[].cc` | string \\\\| null | | CC recipients |\n| `messages[].bcc` | string \\\\| null | | BCC recipients |\n\n### gmail_query_emails (workflow step)\n\n- Search Gmail messages using Gmail search syntax. ## Query syntax Operators (no space after colon): - People: from:, to:, cc:, bcc:, deliveredto: - Content: subject:, "exact phrase", +exact_word, AROUND N (proximity) - Status: is:unread, is:read, is:starred, is:important, is:muted - Labels: label:name, in:inbox, in:sent, in:drafts, in:spam, in:trash, in:anywhere - Category: category:primary, category:social, category:promotions, category:updates, category:forums - Attachments: has:attachment, has:drive, filename:pdf, filename:report.xlsx (has:attachment matches non-inline attachments only \u2014 it misses inline-embedded PDFs, common in receipts/invoices; open the message with gmail_read_email to detect those) - Size: larger:5M, smaller:2M, size:1000000 - Dates (YYYY/MM/DD): after:2024/01/01, before:2024/12/31 - Relative dates: newer_than:7d, older_than:3m, newer_than:1y (d=days, m=months, y=years) Combining: space = AND, OR (uppercase), - (exclude), () for grouping, {} alternative OR syntax. (workflow step, chat)\n- Examples: - from:alice is:unread has:attachment - subject:"quarterly review" newer_than:2d - {from:alice from:bob} subject:project -in:drafts - larger:5M filename:pdf older_than:6m - in:anywhere deliveredto:billing@company.com\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Gmail account to use |\n| `query` | string | yes | Gmail search syntax expression |\n| `max_result` | number | yes | Maximum number of emails to retrieve (1-50) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `messages` | object[] | yes | List of matching Gmail messages |\n| `messages[].id` | string | yes | Gmail message ID |\n| `messages[].threadId` | string | yes | Gmail thread ID |\n| `messages[].labelIds` | string[] | yes | Gmail label IDs (e.g. INBOX, UNREAD) |\n| `messages[].subject` | string | yes | Email subject |\n| `messages[].from` | string | yes | Sender email address |\n| `messages[].to` | string | yes | Recipient email address |\n| `messages[].date` | string | yes | Email date in the workspace timezone, with its offset |\n| `messages[].snippet` | string | yes | Short plain-text preview of the email body |\n| `messages[].cc` | string \\\\| null | | CC recipients |\n| `messages[].bcc` | string \\\\| null | | BCC recipients |\n\n### gmail_query_labels (workflow step)\n\n- List Gmail labels with message and thread counts.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Gmail account to use |\n| `name` | string | | Match labels by name prefix. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `labels` | object[] | yes | |\n| `labels[].id` | string | yes | |\n| `labels[].name` | string | yes | |\n| `labels[].type` | string | yes | system or user |\n| `labels[].messagesTotal` | number | yes | |\n| `labels[].messagesUnread` | number | yes | |\n| `labels[].threadsTotal` | number | yes | |\n| `labels[].threadsUnread` | number | yes | |\n\n### gmail_read_email (workflow step)\n\n- Fetch a Gmail message\'s body and the list of its attachments.\n- Attachments are stored as workspace files \u2014 the result names each one\'s file_id; read one with view_files, attach one to a record with update_records. (chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected account |\n| `message_id` | string | yes | The ID of the Gmail message to retrieve |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message` | object | yes | The full Gmail message |\n| `message.id` | string | | Gmail message ID |\n| `message.threadId` | string | | Gmail thread ID |\n| `message.historyId` | string | | Gmail history ID |\n| `message.labelIds` | string[] | | Gmail label IDs |\n| `message.subject` | string | yes | Email subject |\n| `message.from` | string | yes | Sender email address |\n| `message.to` | string | yes | Recipient email address |\n| `message.date` | string | yes | Email date in the workspace timezone, with its offset |\n| `message.body` | object | yes | Email body |\n| `message.body.type` | "text" \\\\| "html" | yes | Body content type |\n| `message.body.content` | string | yes | Body content |\n| `message.cc` | string \\\\| null | | CC recipients |\n| `message.bcc` | string \\\\| null | | BCC recipients |\n| `message.inReplyTo` | string \\\\| null | | In-Reply-To header |\n| `message.replyTo` | string \\\\| null | | Reply-To header |\n| `message.references` | string[] \\\\| null | | References header |\n| `message.attachments` | object[] | yes | Email attachments |\n| `message.attachments[].partId` | string | yes | MIME part ID |\n| `message.attachments[].filename` | string | yes | Attachment filename as stored, which may differ from the sender\'s |\n| `message.attachments[].mimeType` | string | yes | MIME type as stored, recovered from the filename when the sender declared a generic one |\n| `message.attachments[].attachmentId` | string | yes | Gmail attachment ID |\n| `message.attachments[].file_id` | string | | Stored file ID \u2014 pass to view_files to read it, or to update_records to attach it (chat) |\n| `message.attachments[].size` | number | | Stored size in bytes |\n\n### gmail_reply_email (workflow step)\n\n- Draft or send a reply to a Gmail message.\n- Use reply_body (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `original_message_id` | string | yes | |\n| `reply_body` | string | | Reply body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string | yes | The original sender the reply went to |\n\n### gmail_send_email (workflow step)\n\n- Draft or send a Gmail email.\n- Use content (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `to` | string | | Recipient email(s), comma-separated. Required unless using a template. |\n| `subject` | string | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `content` | string | | Email body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string | yes | The recipients as sent, comma-separated |\n\n## import\n\n### import_rows (workflow step)\n\n- Match a source\'s rows to a table by a key and write what differs: a row whose match fields equal no live row\'s is added, one equalling a row\'s changes the cells that differ, one that changes nothing is counted and not written.\n- It keeps nothing of its own: what it wrote and what it held are its answer, read under the run\'s own reach of the table \u2014 a workflow hands its caller only what it returns. match names the fields that identify a row: text, number or date fields a row is written with, compared as trimmed text, case ignored, and link fields holding one row, compared by the row linked. values holds a row\'s cells by field key, as create_records takes them; a link cell also takes { "lookup": "<text>" }, the linked row found by that text in its table\'s unique or autonumber field, else its title, among the rows the run reads of that table, and a link holding many rows a list of them. (workflow step, chat, MCP, CLI)\n- A row is held, never written, when: it carries hold (its text is the message); several live rows or an earlier row of the call carry its key; a link\'s text names no row, or several; the table refuses it or its row is locked; create is false and no row carries its key; or, under conflicts "hold", a live cell holds another value \u2014 the row\'s empty cells are still written. conflicts "overwrite" writes over a differing cell, "fill_empty" leaves it. conflicts.fields sets one field\'s own.\n- A number and the field its unit or currency is read from are one cell, under the number\'s policy: both written, or both held.\n- A row whose key names a row outside the rows the run may write is held "refused" with nothing of that row: no record_id, no changes.\n- A row carrying remove: true proposes moving the row its key names to the trash: always held, whatever create and conflicts say, and counted unchanged where no row carries its key.\n- A row the source leaves out is never touched.\n- One call takes at most 10000 rows; more is refused with the count, never cut short.\n- Returns { read, created, changed, unchanged, held, written, written_omitted, held_rows, held_omitted }: the rows the call took, the rows it added, the rows it changed a cell of, the rows that changed nothing, and the rows held \u2014 a row can be both changed and held. written lists each record written: record_id, kind ("created" or "changed"), line, and changes \u2014 each field changed, with its field_key, its label and its before and after as text, as a read of the table shows them (an option\'s label, a linked row\'s display, a member\'s name; null where empty).\n- A created row lists no changes: its values are the row\'s own. held_rows lists each row held: kind ("create", "update", "remove", or "unmatched" where no row carries its key and create is false), reason, message, record_id where one row carries its key, key (each match field as text), the changes it would make, links (each link text naming no one row, with the rows it could be as candidates { id, display }), and values \u2014 its key and the cells it would write, as the call gave them.\n- A later call given those values under conflicts { "default": "overwrite" } writes it.\n- The two lists hold at most 10000 entries and 8388608 characters of JSON between them, each in the rows\' order, what was written before what was held; written_omitted and held_omitted count what was left out.\n- A row gives each list at most one entry. dry_run: true runs the whole import, judged as a real one, and rolls it back: the answer says what it would write and hold, with dry_run true, and no row, number, event or workflow run is left behind.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `match` | string[] | yes | |\n| `rows` | object[] | yes | |\n| `rows[].values` | object | yes | |\n| `rows[].hold` | string \\\\| null | | |\n| `rows[].remove` | boolean \\\\| null | | |\n| `rows[].line` | number \\\\| null | | Its line in the source, 1-based. |\n| `create` | boolean \\\\| null | | |\n| `conflicts` | object \\\\| null | | |\n| `dry_run` | boolean \\\\| null | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `dry_run` | true | | The import was judged and rolled back: the lists name what it would write, and no record_id in them exists. |\n| `read` | number | yes | |\n| `created` | number | yes | |\n| `changed` | number | yes | |\n| `unchanged` | number | yes | |\n| `held` | number | yes | |\n| `written` | object[] | yes | |\n| `written[].record_id` | string | yes | |\n| `written[].kind` | "created" \\\\| "changed" | yes | |\n| `written[].line` | number | | |\n| `written[].changes` | object[] | yes | |\n| `written[].changes[].field_key` | string | yes | |\n| `written[].changes[].label` | string | yes | |\n| `written[].changes[].before` | string \\\\| null | yes | |\n| `written[].changes[].after` | string \\\\| null | yes | |\n| `written_omitted` | number | yes | |\n| `held_rows` | object[] | yes | |\n| `held_rows[].line` | number | | |\n| `held_rows[].kind` | "create" \\\\| "update" \\\\| "remove" \\\\| "unmatched" | yes | |\n| `held_rows[].reason` | "stated" \\\\| "removal" \\\\| "conflict" \\\\| "no_match" \\\\| "ambiguous_key" \\\\| "unresolved_link" \\\\| "ambiguous_link" \\\\| "locked" \\\\| "refused" | yes | |\n| `held_rows[].message` | string | yes | |\n| `held_rows[].record_id` | string | | |\n| `held_rows[].key` | object | yes | |\n| `held_rows[].values` | object | yes | |\n| `held_rows[].changes` | object[] | yes | |\n| `held_rows[].changes[].field_key` | string | yes | |\n| `held_rows[].changes[].label` | string | yes | |\n| `held_rows[].changes[].before` | string \\\\| null | yes | |\n| `held_rows[].changes[].after` | string \\\\| null | yes | |\n| `held_rows[].links` | object[] | yes | |\n| `held_rows[].links[].field_key` | string | yes | |\n| `held_rows[].links[].text` | string | yes | |\n| `held_rows[].links[].candidates` | object[] | yes | |\n| `held_rows[].links[].candidates[].id` | string | yes | |\n| `held_rows[].links[].candidates[].display` | string | yes | |\n| `held_omitted` | number | yes | |\n\n## instagram\n\n### instagram_create_post (workflow step)\n\n- Publish to the connected Instagram account.\n- Instagram has no text-only posts.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `caption` | string | yes | |\n| `media_file_ids` | string[] | yes | One image, one video (published as a reel), or 2\u201310 images and videos as a carousel. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## kiotviet\n\n### kiotviet_cancel_invoice (workflow step)\n\n- Cancel a KiotViet sales invoice by id, returning its stock.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `void_payment` | boolean | | Also void payments recorded on the invoice |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `cancelled` | true | yes | |\n\n### kiotviet_cancel_order (workflow step)\n\n- Cancel a KiotViet sales order by id, releasing the stock it reserved.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `void_payment` | boolean | | Also void payments recorded on the order |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `cancelled` | true | yes | |\n\n### kiotviet_create_category (workflow step)\n\n- Create a KiotViet product group (nh\xF3m h\xE0ng), optionally under a parent group.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `name` | string | yes | |\n| `parent_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `category` | object | yes | |\n\n### kiotviet_create_customer (workflow step)\n\n- Create a KiotViet customer (kh\xE1ch h\xE0ng).\n- Returns the customer as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `name` | string | yes | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `contact_number` | string | | |\n| `email` | string | | |\n| `address` | string | | |\n| `location_name` | string | | Province/district as KiotViet names it |\n| `ward_name` | string | | |\n| `gender` | boolean | | true for male, false for female |\n| `birth_date` | string | | yyyy-mm-dd |\n| `group_ids` | number[] | | Customer group ids |\n| `comments` | string | | |\n| `branch_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `customer` | object | yes | |\n\n### kiotviet_create_invoice (workflow step)\n\n- Create a KiotViet sales invoice (h\xF3a \u0111\u01A1n), which deducts stock.\n- Pass order_id to invoice an existing order.\n- Returns the invoice as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `branch_id` | number | yes | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | yes | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | yes | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n| `order_id` | number | | The sales order this invoice settles |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### kiotviet_create_order (workflow step)\n\n- Create a KiotViet sales order (\u0111\u1EB7t h\xE0ng) \u2014 the pre-invoice stage, which reserves stock.\n- Returns the order as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `branch_id` | number | yes | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | yes | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n| `make_invoice` | boolean | | Also issue the invoice at once |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `order` | object | yes | |\n\n### kiotviet_create_product (workflow step)\n\n- Create a KiotViet product (h\xE0ng h\xF3a), optionally with opening stock per branch.\n- Returns the product as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `name` | string | yes | |\n| `code` | string | | Product code (m\xE3 h\xE0ng) |\n| `category_id` | number | yes | Product group id |\n| `base_price` | number | | Selling price |\n| `unit` | string | | |\n| `allows_sale` | boolean | | |\n| `is_active` | boolean | | |\n| `description` | string | | |\n| `weight` | number | | Grams |\n| `inventories` | object[] | | Stock per branch |\n| `inventories[].branch_id` | number | yes | |\n| `inventories[].on_hand` | number | yes | |\n| `inventories[].cost` | number | | Unit cost at this branch |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `product` | object | yes | |\n\n### kiotviet_delete_product (workflow step)\n\n- Delete a KiotViet product by id.\n- KiotViet refuses one that already appears on invoices.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `deleted` | true | yes | |\n\n### kiotviet_get_customer (workflow step)\n\n- Get one KiotViet customer by id or by customer code (m\xE3 kh\xE1ch h\xE0ng).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng), e.g. KH000123 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `customer` | object | yes | |\n\n### kiotviet_get_invoice (workflow step)\n\n- Get one KiotViet sales invoice by id or by invoice code (m\xE3 h\xF3a \u0111\u01A1n), with its line items and payments.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | | |\n| `code` | string | | Invoice code (m\xE3 h\xF3a \u0111\u01A1n), e.g. HD000123 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### kiotviet_get_product (workflow step)\n\n- Get one KiotViet product by id or by product code (m\xE3 h\xE0ng), with stock per branch in inventories[].\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | | |\n| `code` | string | | Product code (m\xE3 h\xE0ng), e.g. SP000123 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `product` | object | yes | |\n\n### kiotviet_list_branches (workflow step)\n\n- List the store\'s KiotViet branches (chi nh\xE1nh).\n- Branch ids scope inventory, invoices and orders.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_categories (workflow step)\n\n- List KiotViet product groups (nh\xF3m h\xE0ng).\n- Each row carries its parentId, so the tree can be rebuilt from the flat list.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_customers (workflow step)\n\n- List KiotViet customers (kh\xE1ch h\xE0ng) with their lifetime totals \u2014 totalInvoiced, totalRevenue, debt, totalPoint.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `name` | string | | Name search |\n| `contact_number` | string | | Phone number |\n| `group_id` | number | | Only customers in this customer group |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_invoices (workflow step)\n\n- List KiotViet sales invoices (h\xF3a \u0111\u01A1n) with their line items in invoiceDetails[] \u2014 productCode, productName, quantity, price, discount, subTotal.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `branch_ids` | number[] | | |\n| `customer_ids` | number[] | | |\n| `customer_code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `status` | number[] | | KiotViet status codes; each row carries statusValue with the label |\n| `from_purchase_date` | string | | Date, yyyy-mm-dd |\n| `to_purchase_date` | string | | Date, yyyy-mm-dd |\n| `include_payment` | boolean | | Include payments[] on each row |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_orders (workflow step)\n\n- List KiotViet sales orders (\u0111\u1EB7t h\xE0ng) \u2014 the pre-invoice stage \u2014 with line items in orderDetails[].\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `branch_ids` | number[] | | |\n| `customer_ids` | number[] | | |\n| `customer_code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `status` | number[] | | KiotViet status codes; each row carries statusValue with the label |\n| `from_purchase_date` | string | | Date, yyyy-mm-dd |\n| `to_purchase_date` | string | | Date, yyyy-mm-dd |\n| `include_payment` | boolean | | Include payments[] on each row |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_products (workflow step)\n\n- List KiotViet products (h\xE0ng h\xF3a) with stock per branch.\n- Each row\'s inventories[] carries branchId, onHand (t\u1ED3n kho), reserved (\u0111\u1EB7t h\xE0ng) and cost.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `category_id` | number | | Only products in this product group |\n| `is_active` | boolean | | Only products still on sale (true) or discontinued (false) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_purchase_orders (workflow step)\n\n- List KiotViet purchase orders (phi\u1EBFu nh\u1EADp h\xE0ng) \u2014 goods received from suppliers \u2014 with line items in purchaseOrderDetails[].\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `branch_ids` | number[] | | |\n| `status` | number[] | | KiotViet status codes; each row carries statusValue with the label |\n| `from_purchase_date` | string | | Date, yyyy-mm-dd |\n| `to_purchase_date` | string | | Date, yyyy-mm-dd |\n| `include_payment` | boolean | | Include payments[] on each row |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_update_category (workflow step)\n\n- Rename a KiotViet product group or move it under another parent.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `name` | string | | |\n| `parent_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `category` | object | yes | |\n\n### kiotviet_update_customer (workflow step)\n\n- Update a KiotViet customer by id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `name` | string | | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `contact_number` | string | | |\n| `email` | string | | |\n| `address` | string | | |\n| `location_name` | string | | Province/district as KiotViet names it |\n| `ward_name` | string | | |\n| `gender` | boolean | | true for male, false for female |\n| `birth_date` | string | | yyyy-mm-dd |\n| `group_ids` | number[] | | Customer group ids |\n| `comments` | string | | |\n| `branch_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `customer` | object | yes | |\n\n### kiotviet_update_invoice (workflow step)\n\n- Update a KiotViet sales invoice by id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `branch_id` | number | | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### kiotviet_update_order (workflow step)\n\n- Update a KiotViet sales order by id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `branch_id` | number | | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `order` | object | yes | |\n\n### kiotviet_update_product (workflow step)\n\n- Update a KiotViet product by id; inventories set the stock level per branch.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `name` | string | | |\n| `code` | string | | Product code (m\xE3 h\xE0ng) |\n| `category_id` | number | | Product group id |\n| `base_price` | number | | Selling price |\n| `unit` | string | | |\n| `allows_sale` | boolean | | |\n| `is_active` | boolean | | |\n| `description` | string | | |\n| `weight` | number | | Grams |\n| `inventories` | object[] | | Stock per branch |\n| `inventories[].branch_id` | number | yes | |\n| `inventories[].on_hand` | number | yes | |\n| `inventories[].cost` | number | | Unit cost at this branch |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `product` | object | yes | |\n\n## lark\n\n### lark_create_records (workflow step)\n\n- Create one or many records in a Lark Base (Bitable) table (max 500 per call). fields are keyed by Lark field name.\n- Returns the new record ids.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `records` | object[] | yes | |\n| `records[].fields` | object | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `created` | number | yes | |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n\n### lark_delete_records (workflow step)\n\n- Delete one or many records from a Lark Base (Bitable) table by record_id (max 500 per call).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `record_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `deleted` | number | yes | |\n| `record_ids` | string[] | yes | |\n\n### lark_get_record (workflow step)\n\n- Get one record from a Lark Base (Bitable) table by its record_id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `record_id` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `record_id` | string | yes | |\n| `fields` | object | yes | |\n\n### lark_list_fields (workflow step)\n\n- List the fields (columns) of a Lark Base (Bitable) table \u2014 name, id, and type.\n- Use to discover the schema before reading or writing records.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `page_size` | number | | |\n| `page_token` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `fields` | object[] | yes | |\n| `fields[].field_id` | string | | |\n| `fields[].field_name` | string | | |\n| `fields[].type` | number | | |\n| `fields[].ui_type` | string | | |\n| `fields[].is_primary` | boolean | | |\n| `has_more` | boolean | yes | |\n| `page_token` | string | | |\n\n### lark_list_records (workflow step)\n\n- List records from a Lark Base (Bitable) table.\n- Returns each record\'s id and fields (keyed by Lark field name).\n- Optionally narrow with filter / sort / field_names / view_id; paginate with page_token.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `filter` | object | | |\n| `filter.conjunction` | "and" \\\\| "or" | yes | |\n| `filter.conditions` | object[] | yes | |\n| `filter.conditions[].field_name` | string | yes | |\n| `filter.conditions[].operator` | string | yes | e.g. is, isNot, contains, isGreater, isEmpty |\n| `filter.conditions[].value` | string[] | | |\n| `sort` | object[] | | |\n| `sort[].field_name` | string | yes | |\n| `sort[].desc` | boolean | | |\n| `field_names` | string[] | | Restrict returned fields by name |\n| `view_id` | string | | |\n| `page_size` | number | | |\n| `page_token` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n| `has_more` | boolean | yes | |\n| `page_token` | string | | |\n| `total` | number | | |\n\n### lark_list_tables (workflow step)\n\n- List the tables in a Lark Base (Bitable) \u2014 id and name.\n- Use to discover which table_id to read or write.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `page_size` | number | | |\n| `page_token` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `tables` | object[] | yes | |\n| `tables[].table_id` | string | | |\n| `tables[].name` | string | | |\n| `has_more` | boolean | yes | |\n| `page_token` | string | | |\n\n### lark_update_records (workflow step)\n\n- Update one or many existing records in a Lark Base (Bitable) table by record_id (max 500 per call). fields are keyed by Lark field name; only provided fields change.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `updated` | number | yes | |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n\n## linkedin\n\n### linkedin_create_post (workflow step)\n\n- Publish a post to the connected LinkedIn member\'s profile.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `text` | string | yes | |\n| `link` | string | | Shown as an article card; needs link_title. A post carries either a link or media. |\n| `link_title` | string | | |\n| `media_file_ids` | string[] | | Up to 20 images, or one video. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## list\n\n### list_banks (workflow step)\n\n- List banks supported by generate_bank_qr_code. (workflow step, chat)\n- Returns the bank code (used as input to the generator), display name, and country.\n- Call this before generate_bank_qr_code to discover valid codes. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `country` | "VN" | | Filter to banks from a specific country. Omit to list all supported banks. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `banks` | object[] | yes | |\n| `banks[].country` | string | yes | |\n| `banks[].code` | string | yes | |\n| `banks[].bin` | string | yes | |\n| `banks[].short_name` | string | yes | |\n| `banks[].full_name` | string | yes | |\n\n## lookup\n\n### lookup_business (workflow step)\n\n- Look up business registration info by tax / registration code.\n- Defaults to Vietnam (country=\'VN\').\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `tax_code` | string | yes | Business tax / registration code. Format varies by country; for VN this is a 10\u201313 digit m\xE3 s\u1ED1 thu\u1EBF. |\n| `country` | string | | ISO country code. Defaults to \'VN\' (Vietnam). Only \'VN\' is supported today. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `country` | string | yes | |\n| `tax_code` | string | yes | |\n| `name` | string | yes | |\n| `address` | string | yes | |\n| `representative` | string \\\\| null | yes | |\n| `status` | string \\\\| null | yes | |\n| `international_name` | string \\\\| null | yes | |\n| `short_name` | string \\\\| null | yes | |\n| `operating_since` | string \\\\| null | yes | |\n| `managed_by` | string \\\\| null | yes | |\n| `company_type` | string \\\\| null | yes | |\n| `primary_industry` | string \\\\| null | yes | |\n\n## misa\n\n### misa_get_dictionary (workflow step)\n\n- Read master-data records from MISA AMIS to look up codes (customer code, item code, warehouse code, etc.) before pushing a voucher.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | MISA connected account to read from |\n| `data_type` | 1 \\\\| 2 \\\\| 3 \\\\| 4 \\\\| 5 \\\\| 6 \\\\| 8 \\\\| 9 \\\\| 10 \\\\| 11 \\\\| 12 | yes | 1 objects (customers/suppliers/employees), 2 inventory items, 3 stocks, 4 units, 5 chart of accounts, 6 organization units, 8 bank accounts, 9 projects, 10 cost objects, 11 payment terms, 12 banks |\n| `skip` | integer | | |\n| `take` | integer | | Max 100 per page |\n| `last_sync_time` | string | | ISO timestamp \u2014 only return records changed since this time |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `success` | boolean | yes | |\n| `data` | object[] | yes | The dictionary records, each as MISA names its fields |\n| `last_sync_time` | string | | Cursor \u2014 pass as last_sync_time on the next call to fetch only newer changes |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_bank_voucher (workflow step)\n\n- Push a bank voucher (deposit / withdrawal / transfer) into MISA AMIS. voucher_type: 1 deposit (b\xE1o c\xF3), 3 withdrawal/debit-advice (b\xE1o n\u1EE3), 2 internal transfer. bank_account_id is the company-side bank account GUID \u2014 look it up with misa_get_dictionary (data_type 8). (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 1 \\\\| 2 \\\\| 3 | yes | |\n| `reftype` | integer | | |\n| `org_refid` | string | yes | a UUID that stays the same for the document \u2014 `uuidFrom(record.id)` |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `bank_account_id` | string | yes | Bank account GUID in MISA \u2014 look up via misa_get_dictionary (workflow step, chat) |\n| `account_object_code` | string | | |\n| `account_object_name` | string | | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `detail` | object[] | yes | |\n| `detail[].description` | string | yes | |\n| `detail[].amount_oc` | number | yes | |\n| `detail[].amount` | number | yes | |\n| `detail[].debit_account` | string | yes | |\n| `detail[].credit_account` | string | yes | |\n| `detail[].sort_order` | integer | | |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_cash_voucher (workflow step)\n\n- Push a cash voucher (cash receipt or cash payment) into MISA AMIS. voucher_type: 5 receipt (thu ti\u1EC1n), 4 payment (chi ti\u1EC1n). detail array carries the journal lines with debit_account/credit_account per Vietnamese chart-of-accounts.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 4 \\\\| 5 | yes | |\n| `reftype` | integer | | |\n| `org_refid` | string | yes | a UUID that stays the same for the document \u2014 `uuidFrom(record.id)` |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `account_object_code` | string | | Counterparty code (customer/supplier/employee) |\n| `account_object_name` | string | | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `detail` | object[] | yes | |\n| `detail[].description` | string | yes | |\n| `detail[].amount_oc` | number | yes | |\n| `detail[].amount` | number | yes | |\n| `detail[].debit_account` | string | yes | |\n| `detail[].credit_account` | string | yes | |\n| `detail[].sort_order` | integer | | |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_dictionary (workflow step)\n\n- Create or update master-data records in MISA AMIS (customers, suppliers, items, warehouses, etc.).\n- Use this before pushing a voucher that references a code MISA doesn\'t yet know about.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n| `dictionary_type` | 1 \\\\| 2 \\\\| 3 \\\\| 4 \\\\| 5 \\\\| 6 \\\\| 7 \\\\| 8 \\\\| 9 \\\\| 10 \\\\| 12 | yes | 1 customers/suppliers/employees, 2 object groups, 3 inventory items, 4 item categories, 5 warehouses, 6 units, 7 bank accounts, 8 banks, 9 expense items, 10 revenue/expense categories, 12 cost allocation |\n| `items` | object[] | yes | Master-data records to create or update. 1\u2013200 per call. Each item\'s required fields depend on dictionary_type \u2014 see MISA Open API docs (chat, MCP, CLI) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `count` | integer | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_purchase_voucher (workflow step)\n\n- Push a purchase voucher (purchase invoice / return / discount-debit-note-to-supplier / service / general purchase / purchase order) into MISA AMIS. voucher_type: 15 invoice, 18 voucher, 17 service, 16 return, 14 discount (debit note to supplier), 21 purchase order. account_object_code identifies the supplier in MISA \u2014 must already exist.\n- MISA sync ack only confirms queueing; final accept/reject arrives async.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 14 \\\\| 15 \\\\| 16 \\\\| 17 \\\\| 18 \\\\| 21 | yes | |\n| `reftype` | integer | | |\n| `org_refid` | string | yes | a UUID that stays the same for the document \u2014 `uuidFrom(record.id)` |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `account_object_code` | string | yes | Supplier code in MISA |\n| `account_object_name` | string | | |\n| `account_object_tax_code` | string | | |\n| `account_object_address` | string | | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `total_vat_amount_oc` | number | yes | |\n| `total_vat_amount` | number | yes | |\n| `detail` | object[] | yes | |\n| `detail[].inventory_item_code` | string | | Item/product code in MISA. Skip when is_description=true |\n| `detail[].inventory_item_name` | string | yes | Item/product display name |\n| `detail[].inventory_item_type` | 0 \\\\| 1 \\\\| 2 \\\\| 3 | | Item type \u2014 0 goods, 1 finished, 2 service, 3 raw material |\n| `detail[].description` | string | | Line description |\n| `detail[].is_description` | boolean | | Description-only line \u2014 when true, only `description` is required |\n| `detail[].stock_code` | string | | Warehouse code in MISA |\n| `detail[].unit_name` | string | | Unit of measure name |\n| `detail[].quantity` | number | yes | Line quantity |\n| `detail[].unit_price` | number | yes | Unit price (in currency_id of voucher) |\n| `detail[].amount_oc` | number | yes | Line total in original currency |\n| `detail[].amount` | number | yes | Line total in accounting currency (= amount_oc when VND) |\n| `detail[].discount_rate` | number | | Line discount percent |\n| `detail[].discount_amount_oc` | number | | Discount amount, original currency |\n| `detail[].discount_amount` | number | | Discount amount, accounting currency |\n| `detail[].vat_rate` | number | yes | VAT percent. 0/5/8/10 for standard, -1 exempt, -2 not creditable, -3 other (set other_vat_rate) |\n| `detail[].other_vat_rate` | number | | Custom VAT percent \u2014 required when vat_rate=-3 |\n| `detail[].vat_amount_oc` | number | yes | VAT amount, original currency |\n| `detail[].vat_amount` | number | yes | VAT amount, accounting currency |\n| `detail[].debit_account` | string | yes | Chart-of-accounts code on debit side, e.g. 131 |\n| `detail[].credit_account` | string | yes | Chart-of-accounts code on credit side, e.g. 511 |\n| `detail[].sale_account` | string | | |\n| `detail[].vat_account` | string | | |\n| `detail[].inventory_account` | string | | |\n| `detail[].sort_order` | integer | | |\n| `detail[].lot_no` | string | | |\n| `detail[].expiry_date` | string | | ISO date |\n| `detail[].organization_unit_code` | string | | Department/branch code |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_sales_voucher (workflow step)\n\n- Push a sales voucher (invoice / sales return / discount-credit-note / sales order) into MISA AMIS. voucher_type: 11 invoice, 13 sales voucher, 12 return, 10 discount/credit note, 20 sales order. reftype refines the sub-type (e.g. 3530 unpaid sale, 3560 sales invoice, 3571 service cash). account_object_code identifies the customer in MISA \u2014 must already exist (use misa_save_dictionary first if not). (workflow step, chat)\n- MISA sync ack only confirms queueing; final accept/reject arrives async on the registered callback URL.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 10 \\\\| 11 \\\\| 12 \\\\| 13 \\\\| 20 | yes | |\n| `reftype` | integer | | Voucher sub-type code \u2014 MISA assigns a default when omitted |\n| `org_refid` | string | yes | a UUID that stays the same for the document \u2014 `uuidFrom(record.id)` |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `account_object_code` | string | yes | Customer code in MISA |\n| `account_object_name` | string | | |\n| `account_object_tax_code` | string | | |\n| `account_object_address` | string | | |\n| `employee_code` | string | | Salesperson code |\n| `employee_name` | string | | |\n| `total_sale_amount_oc` | number | yes | |\n| `total_sale_amount` | number | yes | |\n| `total_discount_amount_oc` | number | | |\n| `total_discount_amount` | number | | |\n| `total_vat_amount_oc` | number | yes | |\n| `total_vat_amount` | number | yes | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `is_sale_with_outward` | boolean | | Auto-generate stock-out slip |\n| `detail` | object[] | yes | |\n| `detail[].inventory_item_code` | string | | Item/product code in MISA. Skip when is_description=true |\n| `detail[].inventory_item_name` | string | yes | Item/product display name |\n| `detail[].inventory_item_type` | 0 \\\\| 1 \\\\| 2 \\\\| 3 | | Item type \u2014 0 goods, 1 finished, 2 service, 3 raw material |\n| `detail[].description` | string | | Line description |\n| `detail[].is_description` | boolean | | Description-only line \u2014 when true, only `description` is required |\n| `detail[].stock_code` | string | | Warehouse code in MISA |\n| `detail[].unit_name` | string | | Unit of measure name |\n| `detail[].quantity` | number | yes | Line quantity |\n| `detail[].unit_price` | number | yes | Unit price (in currency_id of voucher) |\n| `detail[].amount_oc` | number | yes | Line total in original currency |\n| `detail[].amount` | number | yes | Line total in accounting currency (= amount_oc when VND) |\n| `detail[].discount_rate` | number | | Line discount percent |\n| `detail[].discount_amount_oc` | number | | Discount amount, original currency |\n| `detail[].discount_amount` | number | | Discount amount, accounting currency |\n| `detail[].vat_rate` | number | yes | VAT percent. 0/5/8/10 for standard, -1 exempt, -2 not creditable, -3 other (set other_vat_rate) |\n| `detail[].other_vat_rate` | number | | Custom VAT percent \u2014 required when vat_rate=-3 |\n| `detail[].vat_amount_oc` | number | yes | VAT amount, original currency |\n| `detail[].vat_amount` | number | yes | VAT amount, accounting currency |\n| `detail[].debit_account` | string | yes | Chart-of-accounts code on debit side, e.g. 131 |\n| `detail[].credit_account` | string | yes | Chart-of-accounts code on credit side, e.g. 511 |\n| `detail[].sale_account` | string | | |\n| `detail[].vat_account` | string | | |\n| `detail[].inventory_account` | string | | |\n| `detail[].sort_order` | integer | | |\n| `detail[].lot_no` | string | | |\n| `detail[].expiry_date` | string | | ISO date |\n| `detail[].organization_unit_code` | string | | Department/branch code |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n## outlook\n\n### outlook_count_emails (workflow step)\n\n- Count Outlook emails matching a filter or search query.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `search` | string | | KQL expression |\n| `filter` | string | | OData expression |\n| `folder` | string | | Mail folder: well-known name or folder ID |\n| `user_id` | string | | Mailbox owner user ID. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `count` | number | yes | Number of matching emails |\n\n### outlook_query_emails (workflow step)\n\n- Search and filter Outlook emails.\n- Two modes: search (KQL keywords) or structured filters (from, to, subject, dates, etc.).\n- When search is provided, structured filters are ignored. ## KQL search syntax (for the search field) Bare keywords search across from, subject, and body.\n- Property restrictions (no space after colon): - People: from:, to:, cc:, bcc:, participants: (all people fields), recipients: (to+cc+bcc) - Content: subject:, body:, "exact phrase" - Attachments: hasAttachments:true, attachment:filename* - Dates: received:2024-01-01, received:2024-01-01..2024-01-31, received>=2024-01-01, received:today, received:"this week", sent:2024-06-01 - Size (bytes): size>1048576, size:1..10485760 - Status: importance:high, isRead:false, kind:email Combining (UPPERCASE required): AND, OR, NOT, - (exclude), () for grouping.\n- Prefix wildcard: subject:set* (end of word only, no *prefix or *mid*dle).\n- Examples: - from:john@contoso.com AND subject:"quarterly report" - participants:alice importance:high hasAttachments:true - body:contract AND received>=2024-01-01 AND received<=2024-06-30 - (from:legal OR from:compliance) AND subject:contract - attachment:annual* received:"this month"\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `search` | string | | KQL search query \u2014 overrides all structured filter fields when provided |\n| `from` | string | | Sender email address exact match |\n| `to` | string | | Recipient email address exact match |\n| `subject` | string | | Subject starts with this text |\n| `after` | string | | ISO date \u2014 received on or after |\n| `before` | string | | ISO date \u2014 received before |\n| `is_read` | boolean | | |\n| `is_draft` | boolean | | |\n| `has_attachments` | boolean | | |\n| `importance` | "low" \\\\| "normal" \\\\| "high" | | |\n| `is_flagged` | boolean | | Whether the email is flagged for follow-up |\n| `classification` | "focused" \\\\| "other" | | Focused Inbox classification |\n| `category` | string | | Color category name |\n| `conversation_id` | string | | Outlook conversation ID \u2014 retrieves all messages in a thread |\n| `folder` | string | | Mail folder: well-known name (inbox, sentitems, drafts, deleteditems, junkemail, archive) or folder ID |\n| `skip` | number | | Number of results to skip for pagination. |\n| `max_result` | number | yes | Maximum number of emails to retrieve (1-50) |\n| `user_id` | string | | The ID of the user whose mailbox the email belongs to. If not provided, the connected account\'s user ID (me) will be used. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `messages` | object[] | yes | List of matching Outlook messages |\n| `messages[].id` | string | yes | Outlook message ID |\n| `messages[].date` | string | yes | Email date in the workspace timezone, with its offset |\n| `messages[].from` | string | yes | Sender email address |\n| `messages[].subject` | string | yes | Email subject |\n| `messages[].to` | string | yes | Recipient email address |\n| `messages[].cc` | string \\\\| null | yes | CC recipients |\n| `messages[].bcc` | string \\\\| null | yes | BCC recipients |\n| `messages[].snippet` | string | yes | Short plain-text preview of the email body (first ~255 chars) |\n| `messages[].conversationId` | string | | Outlook conversation ID |\n| `messages[].receivedDateTime` | string | | Received date-time in ISO format |\n| `messages[].hasAttachments` | boolean | | Whether the email has attachments |\n| `messages[].importance` | string | | Email importance level |\n| `messages[].isRead` | boolean | | Whether the email has been read |\n| `messages[].isDraft` | boolean | | Whether the email is a draft |\n| `messages[].flagStatus` | string | | Flag status: notFlagged, complete, or flagged |\n| `messages[].classification` | string | | Focused Inbox classification: focused or other |\n| `messages[].categories` | string[] | | Color categories assigned to the email |\n\n### outlook_query_folders (workflow step)\n\n- List mail folders in an Outlook mailbox.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `parent_folder_id` | string | | Parent folder ID to list child folders. Omit for top-level folders. |\n| `name` | string | | Match folders by display name prefix. |\n| `include_hidden` | boolean | | Include hidden system folders |\n| `max_result` | number | | Maximum number of folders to return (1-100) |\n| `skip` | number | | Number of results to skip for pagination |\n| `user_id` | string | | The ID of the user whose mailbox to query. Defaults to the connected account owner. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `folders` | object[] | yes | List of mail folders |\n| `folders[].id` | string | yes | Folder ID |\n| `folders[].displayName` | string | yes | Folder display name |\n| `folders[].parentFolderId` | string | | Parent folder ID |\n| `folders[].childFolderCount` | number | yes | Number of child folders |\n| `folders[].totalItemCount` | number | yes | Total number of items in the folder |\n| `folders[].unreadItemCount` | number | yes | Number of unread items |\n| `folders[].isHidden` | boolean | yes | Whether the folder is hidden |\n\n### outlook_read_email (workflow step)\n\n- Fetch an Outlook email\'s body and the list of its attachments.\n- Attachments are stored as workspace files \u2014 the result names each one\'s file_id; read one with view_files, attach one to a record with update_records. (chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `message_id` | string | yes | The ID of the Outlook message to retrieve |\n| `user_id` | string | | The ID of the user whose mailbox the email belongs to. If not provided, the connected account\'s user ID (me) will be used. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message` | object | yes | The full Outlook message |\n| `message.id` | string | | Outlook message ID |\n| `message.date` | string | yes | Email date in the workspace timezone, with its offset |\n| `message.from` | string | yes | Sender email address |\n| `message.subject` | string | yes | Email subject |\n| `message.to` | string | yes | Recipient email address |\n| `message.cc` | string \\\\| null | yes | CC recipients |\n| `message.bcc` | string \\\\| null | yes | BCC recipients |\n| `message.inReplyTo` | string \\\\| null | yes | In-Reply-To header |\n| `message.replyTo` | string \\\\| null | yes | Reply-To header |\n| `message.references` | string[] \\\\| null | yes | References header |\n| `message.conversationId` | string | | Outlook conversation ID |\n| `message.receivedDateTime` | string | | Received date-time in ISO format |\n| `message.hasAttachments` | boolean | | Whether the email has attachments |\n| `message.importance` | string | | Email importance level |\n| `message.isRead` | boolean | | Whether the email has been read |\n| `message.body` | object | yes | Email body |\n| `message.body.content` | string | yes | Body content |\n| `message.body.contentType` | "text" \\\\| "html" | yes | Body content type |\n| `message.attachments` | object[] | | Email attachments |\n| `message.attachments[].id` | string | yes | Outlook attachment ID |\n| `message.attachments[].fileName` | string | | Attachment filename as stored, which may differ from the sender\'s |\n| `message.attachments[].contentType` | string | | MIME type as stored, recovered from the filename when Graph declared a generic one |\n| `message.attachments[].size` | number | | Stored size in bytes |\n| `message.attachments[].file_id` | string | | Stored file ID \u2014 pass to view_files to read it, or to update_records to attach it (chat) |\n\n### outlook_reply_email (workflow step)\n\n- Draft or send a reply to an Outlook email.\n- Use reply_body (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `original_message_id` | string | yes | |\n| `reply_body` | string | | Reply body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n| `user_id` | string | | Mailbox user ID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string | yes | The original sender the reply went to |\n\n### outlook_send_email (workflow step)\n\n- Draft or send an Outlook email.\n- Use content (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false, and while drafting \'to\' may be omitted to leave the recipient line for the member to fill in.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `to` | string | | Recipient email(s), comma-separated. Required unless using a template. |\n| `subject` | string | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `content` | string | | Email body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n| `user_id` | string | | Mailbox user ID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string \\\\| null | yes | The recipients as sent, comma-separated; null on a draft saved without recipients |\n\n## press\n\n### press_button (workflow step)\n\n- Run a button field\'s action against a record \u2014 equivalent to a user clicking the button in the UI.\n- Buttons are a legacy field type; the action must already be wired.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `record_id` | string | yes | |\n| `field_key` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `execution_id` | string | | |\n| `status` | "success" \\\\| "error" | | |\n| `message` | string | | |\n\n## rename\n\n### rename_file (workflow step)\n\n- Get a file under another name: a new file over the same bytes.\n- Write its file_id into the files field in place of the old one; every other place holding the old file keeps its name.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `filename` | string | yes | The whole new name, keeping the file\'s extension exactly. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n## save\n\n### save_file (workflow step)\n\n- Save a file from a URL to the workspace.\n- Max 20MB.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `url` | string | yes | Public URL to download the file from (https:// or http://). |\n| `filename` | string | | Filename with extension, e.g. \'report.pdf\'. Derived from URL if omitted. |\n| `mime_type` | string | | MIME type, e.g. \'application/pdf\'. Derived from URL response if omitted. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | File ID. |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n### save_record (workflow step)\n\n- Save a record found by its identity, with its lines, all or nothing.\n- A call whose match is carried by no live record adds it; one carried by a record changes the cells of set that differ and appends add_to, so sending the same call twice writes nothing the second time. match holds the record\'s identity: field keys with their values, exactly the fields of one uniqueness its table declares (a unique field, or fields unique together). set holds its other cells. add_to lists items to append to a files, multi-select or many-link field, keeping what it holds. lines saves rows of other tables that link the record.\n- Each entry names the child table_id, its one-row link field to this table, match (the child fields that with link are one uniqueness of the child table) and rows, each stating every match field. link is set by the step and no row states it.\n- A line found by its match changes the cells that differ; one not found is added; a line not listed is left as it is.\n- Any refusal \u2014 a table check, a locked record, a value a field does not take, no write access to a table \u2014 saves nothing.\n- Returns { record_id, created, updated_field_keys, lines }: the cells changed on a record that existed, and per entry of lines the ids it added, changed and left unchanged.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `match` | object | yes | |\n| `set` | object \\\\| null | | |\n| `add_to` | object \\\\| null | | |\n| `lines` | object[] \\\\| null | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `record_id` | string | yes | |\n| `created` | boolean | yes | |\n| `updated_field_keys` | string[] | yes | |\n| `lines` | object[] | yes | |\n| `lines[].table_id` | string | yes | |\n| `lines[].created_ids` | string[] | yes | |\n| `lines[].updated_ids` | string[] | yes | |\n| `lines[].unchanged_ids` | string[] | yes | |\n\n## send\n\n### send_notification (workflow step)\n\n- Notify workspace members.\n- Lotics shows it to each recipient and emails it to their own address, through no connection; a member who signs in with an identifier an admin issued has no mailbox and sees it in Lotics alone.\n- A member is named by the member_id get_workspace kind member lists. (chat, MCP, CLI)\n- In a workflow body, every member is { type: "organization", id: runtime.organization_id } and the member who started the run { type: "member", id: runtime.triggered_by_member_id }; a run a schedule, a webhook or an email starts was started by no member.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `recipients` | any[] | yes | List of recipients. Each entry is a principal: { type: \'member\', id } \\| { type: \'member_group\', id } \\| { type: \'organization\', id } |\n| `title` | string | yes | |\n| `body` | string | yes | |\n| `level` | "urgent" \\\\| "warning" \\\\| "info" \\\\| "success" | | Notification level: urgent, warning, info, success |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message` | string | yes | |\n| `recipients_count` | number | yes | |\n\n## telegram\n\n### telegram_list_chats (workflow step)\n\n- List the chats that messaged a Telegram bot, or added it, in the last 24 hours \u2014 where a chat id comes from.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Telegram connected account |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `chats` | object[] | yes | |\n| `chats[].chat_id` | string | yes | |\n| `chats[].type` | string | yes | private, group, supergroup or channel |\n| `chats[].name` | string | yes | |\n| `chats[].username` | string | | |\n| `chats[].last_seen_at` | string | yes | ISO timestamp of the chat\'s latest update |\n\n### telegram_send_document (workflow step)\n\n- Send a stored file from a Telegram bot to a chat, as a document with an optional caption.\n- The chat must have messaged the bot first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Telegram connected account |\n| `chat_id` | string | yes | Numeric chat id, or the @username of a public channel or group |\n| `file_id` | string | yes | |\n| `caption` | string | | |\n| `parse_mode` | "HTML" \\\\| "MarkdownV2" | | How the text is formatted: Telegram\'s HTML tags or its MarkdownV2 syntax |\n| `disable_notification` | boolean | | Deliver without a notification sound |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | number | yes | |\n| `chat_id` | string | yes | |\n| `sent_at` | string | yes | ISO timestamp |\n\n### telegram_send_message (workflow step)\n\n- Send a text message from a Telegram bot to a chat.\n- The chat must have messaged the bot first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Telegram connected account |\n| `chat_id` | string | yes | Numeric chat id, or the @username of a public channel or group |\n| `text` | string | yes | |\n| `parse_mode` | "HTML" \\\\| "MarkdownV2" | | How the text is formatted: Telegram\'s HTML tags or its MarkdownV2 syntax |\n| `disable_notification` | boolean | | Deliver without a notification sound |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | number | yes | |\n| `chat_id` | string | yes | |\n| `sent_at` | string | yes | ISO timestamp |\n\n## threads\n\n### threads_create_post (workflow step)\n\n- Publish a post to the connected Threads account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `text` | string | yes | |\n| `link` | string | | Shown as a link card. A post carries either a link or media. |\n| `media_file_ids` | string[] | | One image or video, or 2\u201320 images and videos as a carousel. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## vietcombank\n\n### vietcombank_convert_currency (workflow step)\n\n- Convert between currencies using live Vietcombank exchange rates.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `from_currency` | string | yes | Source currency code (e.g., \'USD\', \'EUR\', \'JPY\') |\n| `to_currency` | string | yes | Target currency code (e.g., \'USD\', \'EUR\', \'JPY\') |\n| `amount` | number | yes | The amount to convert (must be positive) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `converted_amount` | number | yes | The converted amount |\n| `exchange_rate` | number | yes | The exchange rate |\n\n## wininvoice\n\n### wininvoice_create_or_update_invoice (workflow step)\n\n- Create or update a Vietnamese e-invoice in WinInvoice. invRef identifies the invoice: a value that already has one resolves to that invoice rather than issuing a new one, and isExisting: 1 means it came back unchanged with nothing issued.\n- Derive invRef from what is being billed rather than from a record that may later describe something else.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invName` | string | yes | Invoice sample name, ex: 1,2,6 |\n| `invSerial` | string | yes | Invoice sign, ex: C24TAA, K24TXX |\n| `invNumber` | string | yes | Invoice number |\n| `invDate` | string | yes | Invoice date (format: yyyy/mm/dd) |\n| `invCustomer` | 0 \\\\| 1 | | Customer is not company? Value 1\\|0 |\n| `invRef` | string | yes | 3rd party Bill-ID. Identifies the invoice: a value that already has an invoice resolves to that invoice instead of issuing a new one |\n| `invRefDate` | string | | Bill date (format: yyyy/mm/dd) |\n| `billNumber` | string | | Bill number - extend |\n| `buyerTax` | string | | Buyer tax-number |\n| `buyerCode` | string | | Buyer-id |\n| `buyerName` | string | yes | Buyer full name |\n| `buyerCompany` | string | | Buyer company/ organization name |\n| `buyerAddress` | string | | Buyer full address |\n| `buyerAcc` | string | | Buyer bank account number |\n| `buyerBank` | string | | Buyer bank name |\n| `buyerEmail` | string | | Buyer email |\n| `buyerPhone` | string | | Buyer phone number |\n| `buyerFax` | string | | Buyer fax number |\n| `buyerCitizenIDNumber` | string | | Citizen Identification Number |\n| `buyerPassportNumber` | string | | Passport Number |\n| `govUnitCode` | string | | Budget-related Unit Code |\n| `invSubTotal` | number | yes | Invoice Subtotal (not include VAT, not include discount) |\n| `invVatRate` | number | yes | VAT Rate: 0,5,8,10 (0%,5%,8%,10%), -1 (Not taxable), -2 (Not declare and pay taxes) |\n| `invVatAmount` | number | yes | VAT Amount |\n| `invTotalAmount` | number | yes | Invoice Total |\n| `invPayment` | string | | Payment method |\n| `invExchangeRate` | number | | Exchange rate (to VietNam \u0111\u1ED3ng) |\n| `invCurrency` | string | | Payment currency |\n| `note` | string | | Note for discount |\n| `invAutoSign` | 0 \\\\| 1 | | Request sign invoice immediately after create successful? Value 1\\|0 |\n| `privateCode` | string | | The UID string (10-12 chars), the buyer will use this code to lookup invoice (after invoice signed). If blank, WinInvoice will generate automatically |\n| `option` | 0 \\\\| 1 \\\\| 2 \\\\| 3 \\\\| 4 | | Invoice type: 0 (Normal invoice), 1 (Information-adjustment invoice), 2 (Increases-adjustment invoice), 3 (Decreases-adjustment invoice), 4 (Replacement invoice - original will be marked as deleted when signed) |\n| `invCodeOld` | string | | Original invoice number (required for adjustment/replacement invoices) |\n| `invNameOld` | string | | Original invoice sample name (required for adjustment/replacement invoices) |\n| `invSignOld` | string | | Original invoice sign (required for adjustment/replacement invoices) |\n| `create04SSHDDT` | 0 \\\\| 1 | | Auto create 04/SS-H\u0110\u0110T? Value 1\\|0 (default 1) |\n| `reason04SSHDDT` | string | | 04/SS-H\u0110\u0110T Reason |\n| `isDscnForSaleInv` | 0 \\\\| 1 | | Apply an 8% tax reduction to the sales invoice? Value 1\\|0 (default 0) |\n| `saleInvVATRate` | number | | Tax rate based on revenue (Only applicable when isDscnForSaleInv = 1) |\n| `saleInvDscnAmnt` | number | | The reduction amount determined based on revenue and corresponding tax rate (Only applicable when isDscnForSaleInv = 1) |\n| `privateNote` | string | | Used for internal notes |\n| `items` | object[] | yes | Array of item/product in this bill |\n| `items[].itemNo` | string | | Item number |\n| `items[].itemCode` | string | | Product/Item code |\n| `items[].itemName` | string | yes | Name of product |\n| `items[].itemPromo` | 0 \\\\| 1 | | Is promotional product (gift)? Value 1\\|0 (default 0) |\n| `items[].isDscnItem` | 0 \\\\| 1 | | Is the discount line? Value 1\\|0 (default 0) |\n| `items[].itemUnit` | string | | Product Unit |\n| `items[].itemQuantity` | number | yes | Product quantity |\n| `items[].itemPrice` | number | yes | Product unit price (not include VAT) |\n| `items[].itemVatRate` | number | | Product VAT rate |\n| `items[].itemVatAmnt` | number | | Product VAT Amount |\n| `items[].itemDscnAmnt` | number | | Product Discount Amount |\n| `items[].itemAmountNoVat` | number | | Product Amount (not include VAT)(not include item discount) |\n| `items[].adjustType` | "PRICE" \\\\| "QTTY" | | Type of adjustment: PRICE (Adjust unit price of item in original invoice), QTTY (Adjust quantity of item in original invoice). This field has effect when value of option field is 2 or 3 |\n| `items[].itemPack` | string | | Product Lot |\n| `items[].itemDate` | string | | Product expiration date |\n| `items[].itemNote` | string | | Product note |\n| `items[].specialGoodsType` | 0 \\\\| 1 \\\\| 2 \\\\| 3 | | Special category of goods: 0 (Normal item/good), 1 (Cars or motorcycles), 2 (Transportation service), 3 (Transportation services on digital and e-commerce platforms) |\n| `items[].specialGoodsInfo` | object | | Data requirements for specialized goods based on specialGoodsType |\n| `items[].specialGoodsInfo.chassisNumb` | string | | Chassis Number (for cars/motorcycles - specialGoodsType = 1) |\n| `items[].specialGoodsInfo.engineNumb` | string | | Engine Number (for cars/motorcycles - specialGoodsType = 1) |\n| `items[].specialGoodsInfo.licensePlate` | string | | License plate of the transport vehicle (for transportation service - specialGoodsType = 2) |\n| `items[].specialGoodsInfo.senderName` | string | | Sender\'s Name (for shipping service - specialGoodsType = 3) |\n| `items[].specialGoodsInfo.senderAddress` | string | | Sender\'s Address (for shipping service - specialGoodsType = 3) |\n| `items[].specialGoodsInfo.senderTaxCode` | string | | Sender\'s Tax Code (for shipping service - specialGoodsType = 3) |\n| `items[].specialGoodsInfo.senderCitizenIDNumber` | string | | Sender\'s Citizen Identification Number (for shipping service - specialGoodsType = 3) |\n| `items[].isNoteItem` | 0 \\\\| 1 | | Is the note line? Value 1\\|0 (default 0) |\n| `connected_account_id` | string | yes | The ID of the connected account |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `oid` | string | yes | WinInvoice\'s UID |\n| `invCode` | string | yes | Invoice number (is \'0000000\' if has not signed) |\n| `invRef` | string | yes | 3rd party Bill-ID |\n| `invSign` | string | yes | Invoice sign, (as invSerial) |\n| `invDate` | string | yes | Invoice date, format yyyy-mm-dd |\n| `invName` | string | yes | Full invoice sample (invName + invSign) |\n| `privateCode` | string | yes | Private lookup code for buyer |\n| `itemTotal` | number | | Count of products in invoice |\n| `itemError` | number | | Count of error-products in invoice |\n| `govTranfer` | number | yes | Invoice has transferred to GOV? 1\\|0 |\n| `govTranID` | string | yes | Transaction ID used to transfer invoice to GOV |\n| `govTranferErr` | number | yes | ansferred to GOV has FAIL? 1\\|0 |\n| `govTranText` | string | yes | Error message if transfer to GOV has FAIL |\n| `govCode` | string | yes | The UID string issue by GOV for this invoice |\n| `autoSign` | number | | Has request sign immediately ? 1\\|0 |\n| `isExisting` | 0 \\\\| 1 | | 1 when invRef already had an invoice and this call returned it unchanged \u2014 no invoice was issued |\n| `link` | string | yes | Link to view the invoice |\n\n## x\n\n### x_create_post (workflow step)\n\n- Publish a post to the connected X account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `text` | string | yes | |\n| `media_file_ids` | string[] | | Up to 4 images, or one GIF, or one video. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## zip\n\n### zip_files (workflow step)\n\n- Bundle multiple files into a zip archive.\n- Max 100 files.\n- An item in file_ids is a file id \u2014 or `{ id, name }` to name that entry in the archive: a file name, never a path, carrying the stored extension.\n- A bare id keeps the stored filename.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_ids` | string \\\\| object[] | yes | |\n| `filename` | string | | Name for the zip file, without the .zip extension. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n';
1796
1796
 
1797
1797
  // docs/app_bindings.md
1798
- var app_bindings_default = "# App bindings \u2014 the queries and workflows an app calls\n\nA custom-code app reaches workspace data through two kinds of binding, each declared on the app\nunder an alias \u2014 a JS identifier (`/^[a-zA-Z_$][a-zA-Z0-9_$]*$/`):\n\n| Binding | Declared with | The app calls it with | An agent calls it with |\n|---|---|---|---|\n| Query | `set_app_queries` (CLI, chat) | `useQuery(\"<alias>\", params?)` | `run_app_query` |\n| Workflow | `set_app_workflow` | `useWorkflow(\"<alias>\")({ ...inputs })` | `run_app_workflow` |\n\nEach runs under the app's authority (`{ type: \"app\", app_id }`), the app owner's reach \u2014 never the\ncalling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next\ncall uses it. `get_app` lists an app's aliases with their params and inputs.\n\nEach reader returns a fingerprint of what it read \u2014 `get_app_workflow`'s `body_sha`, and on the CLI and in chat\n`get_app_query`'s `sha` \u2014 and a write built on that read passes it back:\n`set_app_queries`' `expected_shas` (alias \u2192 `sha`), `set_app_workflow`'s `expected_body_sha`,\n`remove_app_binding`'s `expected_sha`. The write is\nrefused, and writes nothing, when the binding changed since; omit it for an unconditional write.\n\n## Queries\n\nA query declaration is `{ ast, params?, description?, templates? }`:\n\n- `ast` \u2014 a query template (a QueryNode, below). It fixes the tables, filters and columns the alias\n reads, so a caller cannot widen it.\n- `params` \u2014 the typed value holes the caller fills, each an input declaration (see **Inputs**),\n named in the ast as `{{params.<name>}}`. A param fills a VALUE position only \u2014 a filter value, a\n `search` \u2014 never a `table_id` or a field key. Every token the ast names is declared. An optional\n param left unset drops the conditions that read it; a required param that arrives empty is\n refused where a filter reads it.\n- `description` \u2014 one line saying what the query returns, for an agent choosing between aliases.\n- `templates` \u2014 the document templates an export of its rows may fill, by name:\n `{ <name>: { template_id: \"dtl_\u2026\", rows: \"<key the template lists the rows under>\" } }`.\n\nA save checks what a deploy checks: every table is in this workspace and within the owner's reach,\nevery projected, filtered and sorted field resolves, every param token is declared, and a key a\nnode's kind does not take (a stray `sort`, `limit`, `filter`, `search`) is refused by name.\n\nOn the CLI and in chat, `set_app_queries` takes `{ <alias>: <declaration> }`, one alias or several, and merges each: send\nonly the fields you change, `null` clears `params`, `description` or `templates` (never `ast`), and a\nnew alias needs an `ast`. Any other field is refused by name. An alias it omits is kept.\n`remove_app_binding` with `kind: \"query\"` deletes one; `get_app_query` reads one back.\n\n### The query tree\n\nEvery node has a `kind`; all but `from_table` read the node under `from` (`join`: `left`/`right`,\n`union`: `sources`).\n\n| `kind` | Keys |\n|---|---|\n| `from_table` | `table_id`, `filter?`, `search?`, `sort?`, `limit?` |\n| `project` | `columns` \u2014 the output columns |\n| `filter` | `predicate` \u2014 a filter tree over the input's columns |\n| `join` | `left`, `right`, `on: { left_column, right_column }`, `type: \"inner\" \\| \"left\"` |\n| `union` | `sources` (at least two, columns aligned by name and type) |\n| `group` | `by` (columns, `{ bucket: { source, granularity, output } }`, or `{ band: { source, tiers, output } }`), `aggregates` |\n| `window` | `partition_by`, `order_by`, `frame?`, `aggregates?`, `functions?` |\n| `sort` | `by: [{ field_key, order }]` |\n| `limit` | `n`, `offset?` or `keyset?` |\n| `unpivot` | `passthrough`, `row_columns`, `rows` \u2014 one source row into several |\n| `unnest` | `source`, `output`, `display_output?`, `keep_empty?` \u2014 one row per element of a multi-value cell |\n\nFilters (`from_table.filter`, `filter.predicate`, an aggregate's `filter`) are the filter grammar \u2014 the\n`filters` reference \u2014 over the input's columns, taking `{{params.x}}` as values. `search` matches\nevery text-bearing field of the row, accent- and case-insensitive.\n\nA query names a table's fields by the alias its document states, wherever a node reads the table directly (a\n`from_table`'s filter and sort, the `project`, `filter`, `group`, `window`, `unnest` or `unpivot` over it) and a select\ncondition's values by the option's alias. A column a `project` writes answers under the alias it was written with; every\nname past a `project` or an aggregate is that column's own. A `union` whose arms read one alias from different tables\nprojects each arm's columns under one name first. The query is stored by key and read back by alias.\n\nA `project` column is a field's alias as a bare string (output name and type come from the field), or\n`{ output?, type?, source, writable_target?, limit? }` to rename, compute or bound one. A computed\n`source` is a field's alias, `{ literal }`, `{ eq: [a, b] }`, `{ neq: [a, b] }`, `{ isEmpty }`,\n`{ isNotEmpty }`, `{ coalesce: [...] }`, `{ concat: [...] }`, `{ record_id: true }`,\n`{ link: { source, field } }` (a field of the first linked record),\n`{ link_agg: { source, field, operation } }` (`sum`, `avg`, `min`, `max`, `count`, `string_agg`\nover every linked record), or `{ expression }`. A computed column states its `type`: `text`,\n`number`, `boolean`, `date`, `datetime`, `select`, `select_record_link`, `select_member`, `files`\nor `json`. `limit` (1\u201310) bounds a `files` column's entries per cell.\n\nAn aggregate column is `{ output, type, operation, input_column?, filter? }` \u2014 `string_agg` also\ntakes `separator`, `distinct` and `max_values`. A `window` function is\n`{ output, fn }` for `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, plus\n`buckets` for `ntile` and `input_column`, `offset?`, `default?` for `lag` / `lead`; functions need\na non-empty `order_by`.\n\n## Inputs\n\nA query's `params`, a workflow's `inputs` and an agent's `inputs` are one vocabulary: a map of\nname \u2192 `{ type, required?, description?, \u2026per type }`. An entry is required unless it states\n`required: false`.\n\n| `type` | Takes |\n|---|---|\n| `text`, `number`, `boolean`, `date`, `datetime`, `email` | \u2014 |\n| `record_link` | `table_id`; `multi?`; `max?` (with `multi`, the most ids one call carries) |\n| `select` | exactly one of `options: [{ label, value }]` or `field: \"<table id>.<alias>\"` (a select field whose CURRENT options back it); `multi?` |\n| `date_range` | `include_time?` |\n| `member` | `multi?`; `group?` (a member group the value must belong to) |\n| `file` | `multi?` \u2014 an uploaded `fil_\u2026` id, for a files field |\n| `json` | \u2014 any value |\n| `object` | `fields` \u2014 a nested map of entries |\n| `array` | `items` \u2014 one entry |\n\nNesting stops at depth 8. A `field`-form select tracks the field's options as they change; an\ninline `options` set is a fixed list.\n\n## Outputs\n\nA workflow's and an agent's `outputs` declare the structured data handed back: a map of\nname \u2192 `{ type, required?, description?, \u2026per type }`, every entry present unless it states\n`required: false`.\n\n| `type` | Takes |\n|---|---|\n| `text`, `number`, `boolean`, `date`, `datetime`, `email`, `json` | \u2014 |\n| `record_link` | `table_id`, `multi?` |\n| `select` | exactly one of `options: [{ label, value }]` or `field: \"<table id>.<alias>\"`; `multi?` |\n| `object` | `fields` |\n| `array` | `items` |\n\nThe returned value is checked against it, and the app reads it typed. An inline `options` set is\nalso enforced on the value returned, so the producer is told the legal keys; a `field`-form select\nkeeps the type tracking the live field.\n\n## Workflows\n\n`set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,\nname?, description? }`. On an app written as a tree the alias is one the tree names \u2014 a Workflow node's\n`key` or an Act's `workflow` \u2014 and the node states `inputs`, `outputs` and `description`\n(`model/workflow-node`, whose value shapes differ from the **Inputs** below): a call stating any of\nthem for it is refused, and outputs the node leaves out are derived from the body. An alias the tree\nnames nowhere is archived by the next `set_app`. A Workflow node runs only for someone who holds the\napp: on an app shared by a public link, a visitor's call of it is refused as an alias the app does\nnot have. This page's `inputs` and `outputs` are for a custom-code app.\n\n- `source` is the body with no `on({...})` trigger \u2014 the app is the trigger. It reads each declared\n input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id\n with `get_record`.\n The body's steps are the `workflows` reference.\n- `inputs` is the payload the call site passes (see **Inputs**). Omitted, an existing workflow keeps\n its inputs; on a first bind it takes an untyped payload. `{}` clears them.\n- `outputs` is what `return({ data })` hands back (see **Outputs**). Usually omit it: the type is\n derived from the body's `return({ data })`, and the result echoes the bound `outputs`. Declared,\n the body does not save unless its return matches.\n- A save checks the body \u2014 parse, type-check against the declared inputs, names, lint, structure \u2014\n and answers with `[source/code] location: message` lines. `verify_only: true` makes every check\n and writes nothing. A call naming a live alias replaces its body in place, keeping its run history.\n- A call whose payload does not match `inputs` is refused before the body runs.\n\n`get_app_workflow` reads the body back; `set_app_workflow` with `verify_only` and `record_id` or `preview_inputs`\nrehearses a draft before binding; on the CLI and in chat, `remove_app_binding` with\n`kind: \"workflow\"` unbinds it.\n";
1798
+ var app_bindings_default = "# App bindings \u2014 the queries and workflows an app calls\n\nA custom-code app reaches workspace data through two kinds of binding, each declared on the app\nunder an alias \u2014 a JS identifier (`/^[a-zA-Z_$][a-zA-Z0-9_$]*$/`):\n\n| Binding | Declared with | The app calls it with | An agent calls it with |\n|---|---|---|---|\n| Query | `set_app_queries` (CLI, chat) | `useQuery(\"<alias>\", params?)` | `run_app_query` |\n| Workflow | `set_app_workflow` | `useWorkflow(\"<alias>\")({ ...inputs })` | `run_app_workflow` |\n\nEach runs under the app's authority (`{ type: \"app\", app_id }`), the app owner's reach \u2014 never the\ncalling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next\ncall uses it. `get_app` lists an app's aliases with their params and inputs.\n\nEach reader returns a fingerprint of what it read \u2014 `get_app_workflow`'s `body_sha`, and on the CLI and in chat\n`get_app_query`'s `sha` \u2014 and a write built on that read passes it back:\n`set_app_queries`' `expected_shas` (alias \u2192 `sha`), `set_app_workflow`'s `expected_body_sha`,\n`remove_app_binding`'s `expected_sha`. The write is\nrefused, and writes nothing, when the binding changed since; omit it for an unconditional write.\n\n## Queries\n\nA query declaration is `{ ast, params?, description?, templates? }`:\n\n- `ast` \u2014 a query template (a QueryNode, below). It fixes the tables, filters and columns the alias\n reads, so a caller cannot widen it.\n- `params` \u2014 the typed value holes the caller fills, each an input declaration (see **Inputs**),\n named in the ast as `{{params.<name>}}`. A param fills a VALUE position only \u2014 a filter value, a\n `search` \u2014 never a `table_id` or a field key. Every token the ast names is declared. An optional\n param left unset drops the conditions that read it; a required param that arrives empty is\n refused where a filter reads it.\n- `description` \u2014 one line saying what the query returns, for an agent choosing between aliases.\n- `templates` \u2014 the document templates an export of its rows may fill, by name:\n `{ <name>: { template_id: \"dtl_\u2026\", rows: \"<key the template lists the rows under>\" } }`.\n\nA save checks what a deploy checks: every table is in this workspace and within the owner's reach,\nevery projected, filtered and sorted field resolves, every param token is declared, and a key a\nnode's kind does not take (a stray `sort`, `limit`, `filter`, `search`) is refused by name.\n\nOn the CLI and in chat, `set_app_queries` takes `{ <alias>: <declaration> }`, one alias or several, and merges each: send\nonly the fields you change, `null` clears `params`, `description` or `templates` (never `ast`), and a\nnew alias needs an `ast`. Any other field is refused by name. An alias it omits is kept.\n`remove_app_binding` with `kind: \"query\"` deletes one; `get_app_query` reads one back.\n\n### The query tree\n\nEvery node has a `kind`; all but `from_table` read the node under `from` (`join`: `left`/`right`,\n`union`: `sources`).\n\n| `kind` | Keys |\n|---|---|\n| `from_table` | `table_id`, `filter?`, `search?`, `sort?`, `limit?` |\n| `project` | `columns` \u2014 the output columns |\n| `filter` | `predicate` \u2014 a filter tree over the input's columns |\n| `join` | `left`, `right`, `on: { left_column, right_column }`, `type: \"inner\" \\| \"left\"` |\n| `union` | `sources` (at least two, columns aligned by name and type) |\n| `group` | `by` (columns, `{ bucket: { source, granularity, output } }`, or `{ band: { source, tiers, output } }`), `aggregates` |\n| `window` | `partition_by`, `order_by`, `frame?`, `aggregates?`, `functions?` |\n| `sort` | `by: [{ field_key, order }]` |\n| `limit` | `n`, `offset?` or `keyset?` |\n| `unpivot` | `passthrough`, `row_columns`, `rows` \u2014 one source row into several |\n| `unnest` | `source`, `output`, `display_output?`, `keep_empty?` \u2014 one row per element of a multi-value cell |\n\nFilters (`from_table.filter`, `filter.predicate`, an aggregate's `filter`) are the filter grammar \u2014 the\n`filters` reference \u2014 over the input's columns, taking `{{params.x}}` as values. `search` matches\nevery text-bearing field of the row, accent- and case-insensitive.\n\nA query names a table's fields by the alias its document states, wherever a node reads the table directly (a\n`from_table`'s filter and sort, the `project`, `filter`, `group`, `window`, `unnest` or `unpivot` over it) and a select\ncondition's values by the option's alias. A column a `project` writes answers under the alias it was written with; every\nname past a `project` or an aggregate is that column's own. A `union` whose arms read one alias from different tables\nprojects each arm's columns under one name first. The query is stored by key and read back by alias.\n\nA `project` column is a field's alias as a bare string (output name and type come from the field), or\n`{ output?, type?, source, writable_target?, limit? }` to rename, compute or bound one. A computed\n`source` is a field's alias, `{ literal }`, `{ eq: [a, b] }`, `{ neq: [a, b] }`, `{ isEmpty }`,\n`{ isNotEmpty }`, `{ coalesce: [...] }`, `{ concat: [...] }`, `{ record_id: true }`,\n`{ link: { source, field } }` (a field of the first linked record),\n`{ link_agg: { source, field, operation } }` (`sum`, `avg`, `min`, `max`, `count`, `string_agg`\nover every linked record), or `{ expression }`. A computed column states its `type`: `text`,\n`number`, `boolean`, `date`, `datetime`, `select`, `select_record_link`, `select_member`, `files`\nor `json`. `limit` (1\u201310) bounds a `files` column's entries per cell.\n\nAn aggregate column is `{ output, type, operation, input_column?, filter? }` \u2014 `string_agg` also\ntakes `separator`, `distinct` and `max_values`. A `window` function is\n`{ output, fn }` for `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, plus\n`buckets` for `ntile` and `input_column`, `offset?`, `default?` for `lag` / `lead`; functions need\na non-empty `order_by`.\n\n## Inputs\n\nA query's `params`, a workflow's `inputs` and an agent's `inputs` are one vocabulary: a map of\nname \u2192 `{ type, required?, description?, \u2026per type }`. An entry is required unless it states\n`required: false`.\n\n| `type` | Takes |\n|---|---|\n| `text`, `number`, `boolean`, `date`, `datetime`, `email` | \u2014 |\n| `record_link` | `table_id`; `multi?`; `max?` (with `multi`, the most ids one call carries) |\n| `select` | exactly one of `options: [{ label, value }]` or `field: \"<table id>.<alias>\"` (a select field whose CURRENT options back it); `multi?` |\n| `date_range` | `include_time?` |\n| `member` | `multi?`; `group?` (a member group the value must belong to) |\n| `file` | `multi?` \u2014 an uploaded `fil_\u2026` id, for a files field |\n| `json` | \u2014 any value |\n| `object` | `fields` \u2014 a nested map of entries |\n| `array` | `items` \u2014 one entry |\n\nNesting stops at depth 8. A `field`-form select tracks the field's options as they change; an\ninline `options` set is a fixed list.\n\n## Outputs\n\nA workflow's and an agent's `outputs` declare the structured data handed back: a map of\nname \u2192 `{ type, required?, description?, \u2026per type }`, every entry present unless it states\n`required: false`.\n\n| `type` | Takes |\n|---|---|\n| `text`, `number`, `boolean`, `date`, `datetime`, `email`, `json` | \u2014 |\n| `record_link` | `table_id`, `multi?` |\n| `select` | exactly one of `options: [{ label, value }]` or `field: \"<table id>.<alias>\"`; `multi?` |\n| `object` | `fields` |\n| `array` | `items` |\n\nThe returned value is checked against it, and the app reads it typed. An inline `options` set is\nalso enforced on the value returned, so the producer is told the legal keys; a `field`-form select\nkeeps the type tracking the live field.\n\n## Workflows\n\n`set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,\nname?, description? }`. On an app written as a tree the alias is one the tree names \u2014 a Workflow node's\n`key` or an Act's `workflow` \u2014 and the node states `inputs`, `outputs` and `description`\n(`reference/workflow-node`, whose value shapes differ from the **Inputs** below): a call stating any of\nthem for it is refused, and outputs the node leaves out are derived from the body. An alias the tree\nnames nowhere is archived by the next `set_app`. A Workflow node runs only for someone who holds the\napp: on an app shared by a public link, a visitor's call of it is refused as an alias the app does\nnot have. This page's `inputs` and `outputs` are for a custom-code app.\n\n- `source` is the body with no `on({...})` trigger \u2014 the app is the trigger. It reads each declared\n input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id\n with `get_record`.\n The body's steps are the `workflows` reference.\n- `inputs` is the payload the call site passes (see **Inputs**). Omitted, an existing workflow keeps\n its inputs; on a first bind it takes an untyped payload. `{}` clears them.\n- `outputs` is what `return({ data })` hands back (see **Outputs**). Usually omit it: the type is\n derived from the body's `return({ data })`, and the result echoes the bound `outputs`. Declared,\n the body does not save unless its return matches.\n- A save checks the body \u2014 parse, type-check against the declared inputs, names, lint, structure \u2014\n and answers with `[source/code] location: message` lines. `verify_only: true` makes every check\n and writes nothing. A call naming a live alias replaces its body in place, keeping its run history.\n- A call whose payload does not match `inputs` is refused before the body runs.\n\n`get_app_workflow` reads the body back; `set_app_workflow` with `verify_only` and `record_id` or `preview_inputs`\nrehearses a draft before binding; on the CLI and in chat, `remove_app_binding` with\n`kind: \"workflow\"` unbinds it.\n";
1799
1799
 
1800
1800
  // docs/document_templates.md
1801
- var document_templates_default = '# Document Templates\n\nTurn workspace data into finished documents \u2014 invoices, contracts, reports, letters,\nlabels, emails \u2014 by filling a reusable template instead of writing a file from scratch.\nYou register a template once, then generate a filled file (or a rendered email) from it as\nmany times as you like, feeding it data from records, a workflow, or an agent run.\n\nThis is a **capability + usage** guide. A tool\'s exact input schema is in its own description.\n\n## The five template types\n\n`set_template` writes all five. Its `type` names which one a new template is.\n\n| Type | What it fills | Output | `type` |\n|---|---|---|---|\n| `pdf-form` | fields overlaid on an **uploaded PDF** at fixed positions | PDF | `"pdf-form"` |\n| `html` (a PDF) | an **HTML + Handlebars** layout you write from scratch | PDF | `"html"` |\n| `excel` | an **uploaded `.xlsx`** with `{{marker}}` cells | `.xlsx` | `"excel"` |\n| `word` | an **uploaded `.docx`** with `{{marker}}`s | `.docx` | `"word"` |\n| `email` | an **inline HTML + Handlebars** body, rendered when the email is sent | email | `"email"` |\n\nTwo shapes underneath: **file-backed** (`pdf-form`, `excel`, `word`) clone an uploaded\noffice file and mark it up; **inline** (`html`, `email`) store the markup you author directly.\n\n## The lifecycle: create \u2192 generate \u2192 chain\n\n1. **Create** a template \u2014 register the file (or inline markup) and declare a `variables`\n map (the named slots the template fills). This returns a template id (`dtl_\u2026`).\n2. **Generate** a filled file \u2014 call `generate_document` with the\n template id, a `filename` (no extension), and a `data` map keyed by your variable names.\n It substitutes the markers and returns a **generated file** (with a `file_id`).\n3. **Chain** the result \u2014 generation only *produces* the file. Attaching it to a record,\n sending it, or saving it is a separate step: `update_records` with `add_to` on a files field\n files the `file_id` on a record (`remove_made`, a template id, takes out the paper that template\n made earlier); over MCP and on the CLI, `download_file` fetches it; inside a workflow, pass the `file_id` to the next\n step. A paper an app makes is its Act\'s `template`, which files it on the record itself\n (below).\n\nEmail is the exception: `generate_document` refuses an email template. An email template is\nrendered to a subject + body **at send time** from the template and the data the send step\nsupplies \u2014 so you author it with `set_template` and a workflow/app send step (or the\nweb composer) does the rendering and delivery. External agents send their own email directly.\n\n## Creating and changing a template\n\nA paper prints the owner\'s own terms only: the clauses, wording and fields of the sample they sent, never a term the owner did not state.\n\n`set_template` creates a template when it is given no `template_id`, and changes the named one\nwhen it is given one. Creating takes `type` and `name`, and the fields of its type; changing takes\n`expected_version` \u2014 the `current_version` that `get_template` returned, so a concurrent edit is\nrefused rather than overwritten \u2014 and only the fields to change. A field the type does not take is\nrefused rather than dropped. `description` (one line on what the template produces) is on every type.\n\n### Getting a file in\n\nA file-backed template starts from a file in the workspace: over MCP and on the CLI, `upload_file` takes a public URL,\nor bytes you hold, and returns a `file_id`. When the person has the file and your host shows you its content but not its\nbytes, `upload_file` with `person: true` returns a `drop_url` for them to open and a `ticket_id` per file; once they say\nthey dropped it, `upload_file` with that `ticket_id` returns the `file_id`.\n\nA new Word or Excel paper is a file you build yourself, in your own filesystem or sandbox, with its markers typed where\nthe values go, then, over MCP or on the CLI, send it with `upload_file` `files` and register with `set_template`. Where your sandbox cannot reach\nthis host, give the person the file and a `person` drop link.\n\n### By type\n\n- **`html`** \u2014 a full HTML document with inline CSS and `{{variable}}` placeholders, rendered to PDF; anything whose layout\n you control. `variables` are plain text, rich `html`, an auto-rendered `table` or a `list`; `format` (page size) and\n `orientation` are optional. A change may send `patch_ops`, exact search-and-replace edits of the stored markup, in place\n of the whole `template`.\n- **`pdf-form`** \u2014 an existing PDF filled in place. `set_template` with the uploaded `template_file_id`;\n `analyze_pdf_template` reads the labels and positions off the page; `set_template` again with `expected_version` and the\n positioned `variables` (`text` / `checkbox` / `image`, each with a page and x/y/size). Data is one scalar per field.\n- **`excel`** \u2014 a `.xlsx` with `{{marker}}`s in the cells to fill, and a `variables` map. Markers are validated at create\n time: a structural error blocks the save, and a declared variable with no marker comes back in `unmarked_variables`\n (skipped). Over MCP and on the CLI, `validate_excel_template` checks a file\'s markers without creating a template. Pass\n raw numbers, ISO dates and booleans at generate time; the cell\'s number format shows them.\n- **`word`** \u2014 a `.docx` with `{{marker}}`s. `variables` is optional: every marker is derived (`{{name}}` string,\n `{{FOR \u2026 IN name}}` array, `{{IF name}}` boolean), and a marker you did not declare is still added\n (`derived_variables`). At generate time `data` needs a key for **every** marker, `""` for a blank one. A marker may run\n across formatting runs and is still read whole. In a table, the row holding `{{FOR line IN lines}}` and the row holding\n `{{END-FOR line}}` enclose the row that repeats.\n- **`email`** \u2014 inline HTML + Handlebars, with `template` and `variables`; optionally a default `subject` (it takes\n `{{variable}}` too) and `default_to`, `default_cc`, `default_bcc`. Body and subject render from the data at send time.\n\n## Markers, at a usage level\n\nEvery type supports three shapes of substitution. The exact syntax differs per engine \u2014 read\nthe `set_template` description for the reference; here is the capability:\n\n- **Scalar** \u2014 a single value in one spot: `{{name}}`, `{{total}}`. Whole-cell scalars in\n Excel preserve their type via the cell\'s number format.\n- **Repeating rows / line items** \u2014 one template row rendered once per item in a list, for\n invoice lines, order rows, tables. Excel and the HTML/email engines use a Handlebars-style\n `{{#each items}}\u2026{{/each}}`; Word uses `{{FOR item IN items}}\u2026{{$item.field}}\u2026{{END-FOR item}}`. The HTML and email types\n also offer an auto-rendered `table` variable \u2014 declare its columns and pass an array, no\n hand-written loop. (Excel always uses the `{{#each}}` marker rows \u2014 it has no auto-rendered\n `table` type; its variable types are string/number/date/boolean/array.) Inside a loop the row\'s number counted from 1\n is `{{@ordinal}}` (Excel, HTML, email) or `{{$ordinal}}` (Word), the innermost loop\'s: an STT column is a cell holding\n `{{@ordinal}}` beside the row\'s other markers. `{{@index}}` counts from 0.\n- **Conditional sections** \u2014 a block shown only when a condition holds (a "paid" stamp, an\n optional notes block). Excel/HTML/email use `{{#if}}\u2026{{else}}\u2026{{/if}}`; Word uses `{{IF name}}\u2026{{END-IF name}}`.\n\nThe `html` and `email` types print a raw value through a helper in the template\'s `locale` (`vi` or `en`; none prints in the organization\'s language): `{{money total "VND"}}`, `{{number qty}}`, `{{date issued_on}}`, `{{words total "USD"}}` (`model/groups-and-templates` lists what each prints). `{{date (add_days issued_on -1)}}` prints the day before, `add_days` shifting a date by whole days. A value a helper cannot read fails the render.\n\nA field can also print as a **QR code** or a **Code 128 barcode** (a picture a phone\'s camera or a scanner reads back): `{{qr link}}` or `{{code128 position 6}}` in html (never email: mail clients do not show the picture, and a save refuses it), `{{position | qr}}` or `{{position | code128:6}}` as a whole Excel cell (its row grows to the picture), `{{IMAGE qr(position)}}` or `{{IMAGE code128(position, 6)}}` in Word. The optional number is the picture\'s width in centimetres. Without it the width follows the value\'s length \u2014 each module prints 0.5 mm in a QR code and 0.33 mm in a barcode, so a longer value prints wider; a width that would shrink a module below what a scanner reads (0.3 mm, 0.19 mm) is refused, naming the least width that scans. An empty field draws nothing.\n\nA record\'s **photos** (a files field, an act\'s paper hands each as a file cell `{id, name, mime, url}`) print too, and only for a file whoever makes the paper may read \u2014 the same rule the file proxy applies; any other file is never drawn. html: `{{#each photos}}<img src="{{url}}">{{/each}}` (only the data\'s file cells are inlined; a url typed into a text field stays a link). Word: `{{IMAGE picture(cover, 8)}}`, and `{{FOR p IN photos}}{{IMAGE picture($p, 8)}}{{END-FOR p}}` for several. Excel: `{{cover | picture:6}}` as the whole cell, or `{{this | picture}}` inside `{{#each photos}}` (each row grows to its picture). The number is the width in centimetres (8, or 6 in Excel, by default). `picture` takes one image file: a list (loop over it), a file that is no image or one that cannot be read is refused by name. Photos are embedded at most 1600 pixels across, about 200 dpi at 20 cm.\n\nA Word command (`{{ \u2026 }}`) reads the data and calls, by name, a Word helper (`picture`, `qr`, `code128`) or one of these text\nand number functions: `trim`, `trimStart`, `trimEnd`, `lower`, `upper`, `capitalize`, `replace`, `replaceAll`, `removeChars`, `removeSpaces`,\n`startsWith`, `endsWith`, `contains`, `substring`, `padStart`, `padEnd`, `length`, `join`, `split`, `repeat`, `formatNumber`,\n`formatDecimal`, `numberToWords`. It holds no JavaScript method or regular-expression literal: `{{upper(name)}}`,\n`{{formatDecimal(total, 0, "vi-VN")}}`, and `{{removeSpaces(removeChars(cccd || "", ".\u2026"))}}` for the text without its dots,\nellipses and spaces. `removeChars(text, chars)` removes every character of `text` that occurs in `chars` (a set of characters,\nnever a pattern); `removeSpaces(text)` removes every whitespace character of `text`, anywhere in it, the set `trim` trims.\n\nA field named like a function is read as the field, and calling the function then fails.\n\n`set_template`\'s description holds each type\'s exact marker grammar and helper list.\n\n## Generating a filled file\n\n```\nget_template {"template_id": "dtl_\u2026"} # its declared variables\ngenerate_document {"document_template_id": "dtl_\u2026", "filename": "invoice-1042",\n "data": {"company_name": "Acme", "total": 1042, "items": [...]}}\n```\n\n- `data` is a map keyed by your variable names. Scalars for scalar/form fields; arrays for\n `table`/`list`/loop variables. `filename` excludes the extension (the type sets it). Pass a select, a\n member or a link as `get_record`\'s `display` words, a number and a date raw.\n- `generate_document` renders the template\'s own type and returns a generated file object; take\n its `file_id` onward.\n- A PDF no template is wanted for takes `html`, a `filename` and its `data` in place of `document_template_id`: the markup is\n rendered as an `html` template is (the same markup rules, markers and helpers) and the PDF is stored as a workspace file. Name one of the two.\n- In a workflow, a **generate step** calls the same tool and hands the `file_id` to the next\n step (attach to a record, send as an attachment, etc.).\n\n## A template an app\'s Act makes\n\nAn Act with `template` (or `templates`) fills an `html`, `excel` or `word` template from its record and files the result\n(a PDF, an `.xlsx` or a `.docx`) in the record\'s files field `into`; `run_app_workflow` on `act_<key>` (or the `workflow` the Act names) presses it. The Act hands the template the record\'s fields by their\nalias, so name each variable after one: `{{customer}}`, `{{total}}`. A select, a member and a link arrive as their words\n(a link as the linked row\'s title); a number and a date arrive raw, for `{{money total "VND"}}` and `{{date issued_on}}`.\nThe rows of each child the record draws arrive under the name of the record\'s own link to them, for a loop:\n\n```html\n<h1>Quote {{code}}</h1>\n<p>{{customer}} \u2014 {{money total "VND"}}</p>\n{{#each lines}}<p>{{description}} \xD7 {{quantity}}</p>{{/each}}\n```\n\nHere `code`, `customer` and `total` are fields of the record and `lines` is its link to the child table. The rows\nalso arrive under the child table\'s `ref` (`quote_line`), so `{{#each quote_line}}` fills as well; a Word template loops the same rows with `{{FOR line IN lines}}` \u2026\n`{{$line.description}}` \u2026 `{{END-FOR line}}`.\n\nEvery List the record draws and the template names (an html template\'s variables, a Word template\'s markers) hands its\nrows, each List\'s `where` applied; a List the template does not name is not read. A List that only reads hands them under its\n`key` (the table\'s alias where it states none), and a List that sums a number hands the sums under `<name>_totals`, one\nentry per number the List draws as a column or as the table\'s figure. A `where` bound may be `{ "record": "<field>" }`,\na date or number the record itself holds, so a record can print a window of its own days. A service plan row (customer,\n`period_start`, `period_end`) draws the customer\'s visits within its period:\n\n```\n{ "type": "List", "key": "visits", "entity": "visit", "same": "customer", "label": "Visits", "columns": ["visited_on", "hours"],\n "where": { "visited_on": { "gte": { "record": "period_start" }, "lte": { "record": "period_end" } } } }\n```\n\n```html\n{{#each visits}}<p>{{visited_on}} \u2014 {{hours}} h</p>{{/each}}\n<p>Total {{visits_totals.hours}} h</p>\n```\n\nA bound the record leaves blank, or holds as anything but one whole day (`2026-05`) or one figure, keeps no row:\n`visits` is empty and `visits_totals.hours` is `0`. The sums add a number as the List\'s foot does: a cell holding a\nfigure adds it, any other cell (blank, an errored formula, a lookup\'s list) adds nothing. A Word template loops\n`{{FOR v IN visits}}` and prints `{{visits_totals.hours}}` the same way. A template that names a List narrowed to the\nreader (`"me"`) is refused, and so is a name a rows List and a `list` List both reach: give the `list` a `key`. A record\nbound narrows what is listed and is never an access bound; the table\'s row rules are.\n\nA paper over many rows (a client\'s month, a statement) is that: a record of the period whose List is windowed by its dates, made by the Act\'s `template`; or a List `Export` with `template` and `per` (`model/export-node`).\n`model/groups-and-templates` has the rest.\n\n## Discovering and managing templates\n\nThese tools work across all five types:\n\n| Tool | Does |\n|---|---|\n| `get_workspace` | list the workspace\'s templates (`kind: "template"`), each with its id, type and one line |\n| `get_template` | one template\'s full definition, incl. the `variables` it expects and the `current_version` a change needs |\n| `set_template` | create a template, or change one |\n| `clone_template` | copy a template to iterate on, in chat |\n| `delete_template` | remove a template |\n\nCall `get_template` before generating when you don\'t already know a template\'s variable names.\n';
1801
+ var document_templates_default = '# Document Templates\n\nTurn workspace data into finished documents \u2014 invoices, contracts, reports, letters,\nlabels, emails \u2014 by filling a reusable template instead of writing a file from scratch.\nYou register a template once, then generate a filled file (or a rendered email) from it as\nmany times as you like, feeding it data from records, a workflow, or an agent run.\n\nThis is a **capability + usage** guide. A tool\'s exact input schema is in its own description.\n\n## The five template types\n\n`set_template` writes all five. Its `type` names which one a new template is.\n\n| Type | What it fills | Output | `type` |\n|---|---|---|---|\n| `pdf-form` | fields overlaid on an **uploaded PDF** at fixed positions | PDF | `"pdf-form"` |\n| `html` (a PDF) | an **HTML + Handlebars** layout you write from scratch | PDF | `"html"` |\n| `excel` | an **uploaded `.xlsx`** with `{{marker}}` cells | `.xlsx` | `"excel"` |\n| `word` | an **uploaded `.docx`** with `{{marker}}`s | `.docx` | `"word"` |\n| `email` | an **inline HTML + Handlebars** body, rendered when the email is sent | email | `"email"` |\n\nTwo shapes underneath: **file-backed** (`pdf-form`, `excel`, `word`) clone an uploaded\noffice file and mark it up; **inline** (`html`, `email`) store the markup you author directly.\n\n## The lifecycle: create \u2192 generate \u2192 chain\n\n1. **Create** a template \u2014 register the file (or inline markup) and declare a `variables`\n map (the named slots the template fills). This returns a template id (`dtl_\u2026`).\n2. **Generate** a filled file \u2014 call `generate_document` with the\n template id, a `filename` (no extension), and a `data` map keyed by your variable names.\n It substitutes the markers and returns a **generated file** (with a `file_id`).\n3. **Chain** the result \u2014 generation only *produces* the file. Attaching it to a record,\n sending it, or saving it is a separate step: `update_records` with `add_to` on a files field\n files the `file_id` on a record (`remove_made`, a template id, takes out the paper that template\n made earlier); over MCP and on the CLI, `download_file` fetches it; inside a workflow, pass the `file_id` to the next\n step. A paper an app makes is its Act\'s `template`, which files it on the record itself\n (below).\n\nEmail is the exception: `generate_document` refuses an email template. An email template is\nrendered to a subject + body **at send time** from the template and the data the send step\nsupplies \u2014 so you author it with `set_template` and a workflow/app send step (or the\nweb composer) does the rendering and delivery. External agents send their own email directly.\n\n## Creating and changing a template\n\nA paper prints the owner\'s own terms only: the clauses, wording and fields of the sample they sent, never a term the owner did not state.\n\n`set_template` creates a template when it is given no `template_id`, and changes the named one\nwhen it is given one. Creating takes `type` and `name`, and the fields of its type; changing takes\n`expected_version` \u2014 the `current_version` that `get_template` returned, so a concurrent edit is\nrefused rather than overwritten \u2014 and only the fields to change. A field the type does not take is\nrefused rather than dropped. `description` (one line on what the template produces) is on every type.\n\n### Getting a file in\n\nA file-backed template starts from a file in the workspace: over MCP and on the CLI, `upload_file` takes a public URL,\nor bytes you hold, and returns a `file_id`. When the person has the file and your host shows you its content but not its\nbytes, `upload_file` with `person: true` returns a `drop_url` for them to open and a `ticket_id` per file; once they say\nthey dropped it, `upload_file` with that `ticket_id` returns the `file_id`.\n\nA new Word or Excel paper is a file you build yourself, in your own filesystem or sandbox, with its markers typed where\nthe values go, then, over MCP or on the CLI, send it with `upload_file` `files` and register with `set_template`. Where your sandbox cannot reach\nthis host, give the person the file and a `person` drop link.\n\n### By type\n\n- **`html`** \u2014 a full HTML document with inline CSS and `{{variable}}` placeholders, rendered to PDF; anything whose layout\n you control. `variables` are plain text, rich `html`, an auto-rendered `table` or a `list`; `format` (page size) and\n `orientation` are optional. A change may send `patch_ops`, exact search-and-replace edits of the stored markup, in place\n of the whole `template`.\n- **`pdf-form`** \u2014 an existing PDF filled in place. `set_template` with the uploaded `template_file_id`;\n `analyze_pdf_template` reads the labels and positions off the page; `set_template` again with `expected_version` and the\n positioned `variables` (`text` / `checkbox` / `image`, each with a page and x/y/size). Data is one scalar per field.\n- **`excel`** \u2014 a `.xlsx` with `{{marker}}`s in the cells to fill, and a `variables` map. Markers are validated at create\n time: a structural error blocks the save, and a declared variable with no marker comes back in `unmarked_variables`\n (skipped). Over MCP and on the CLI, `validate_excel_template` checks a file\'s markers without creating a template. Pass\n raw numbers, ISO dates and booleans at generate time; the cell\'s number format shows them.\n- **`word`** \u2014 a `.docx` with `{{marker}}`s. `variables` is optional: every marker is derived (`{{name}}` string,\n `{{FOR \u2026 IN name}}` array, `{{IF name}}` boolean), and a marker you did not declare is still added\n (`derived_variables`). At generate time `data` needs a key for **every** marker, `""` for a blank one. A marker may run\n across formatting runs and is still read whole. In a table, the row holding `{{FOR line IN lines}}` and the row holding\n `{{END-FOR line}}` enclose the row that repeats. A newline in a value prints as a line break and a tab as a tab;\n a tab the paper prints only when a blank is empty is written `{{IF !name}}{{"\\t"}}\u2026\u2026{{END-IF}}`, since the\n form\'s own tab stays put inside an `{{IF}}` on the same line.\n- **`email`** \u2014 inline HTML + Handlebars, with `template` and `variables`; optionally a default `subject` (it takes\n `{{variable}}` too) and `default_to`, `default_cc`, `default_bcc`. Body and subject render from the data at send time.\n\n## Markers, at a usage level\n\nEvery type supports three shapes of substitution. The exact syntax differs per engine \u2014 read\nthe `set_template` description for the reference; here is the capability:\n\n- **Scalar** \u2014 a single value in one spot: `{{name}}`, `{{total}}`. Whole-cell scalars in\n Excel preserve their type via the cell\'s number format.\n- **Repeating rows / line items** \u2014 one template row rendered once per item in a list, for\n invoice lines, order rows, tables. Excel and the HTML/email engines use a Handlebars-style\n `{{#each items}}\u2026{{/each}}`; Word uses `{{FOR item IN items}}\u2026{{$item.field}}\u2026{{END-FOR item}}`. The HTML and email types\n also offer an auto-rendered `table` variable \u2014 declare its columns and pass an array, no\n hand-written loop. (Excel always uses the `{{#each}}` marker rows \u2014 it has no auto-rendered\n `table` type; its variable types are string/number/date/boolean/array.) Inside a loop the row\'s number counted from 1\n is `{{@ordinal}}` (Excel, HTML, email) or `{{$ordinal}}` (Word), the innermost loop\'s: an STT column is a cell holding\n `{{@ordinal}}` beside the row\'s other markers. `{{@index}}` counts from 0.\n- **Conditional sections** \u2014 a block shown only when a condition holds (a "paid" stamp, an\n optional notes block). Excel/HTML/email use `{{#if}}\u2026{{else}}\u2026{{/if}}`; Word uses `{{IF name}}\u2026{{END-IF name}}`.\n\nThe `html` and `email` types print a raw value through a helper in the template\'s `locale` (`vi` or `en`; none prints in the organization\'s language): `{{money total "VND"}}`, `{{number qty}}`, `{{date issued_on}}`, `{{words total "USD"}}` (`reference/groups-and-templates` lists what each prints). `{{date (add_days issued_on -1)}}` prints the day before, `add_days` shifting a date by whole days. A value a helper cannot read fails the render.\n\nA field can also print as a **QR code** or a **Code 128 barcode** (a picture a phone\'s camera or a scanner reads back): `{{qr link}}` or `{{code128 position 6}}` in html (never email: mail clients do not show the picture, and a save refuses it), `{{position | qr}}` or `{{position | code128:6}}` as a whole Excel cell (its row grows to the picture), `{{IMAGE qr(position)}}` or `{{IMAGE code128(position, 6)}}` in Word. The optional number is the picture\'s width in centimetres. Without it the width follows the value\'s length \u2014 each module prints 0.5 mm in a QR code and 0.33 mm in a barcode, so a longer value prints wider; a width that would shrink a module below what a scanner reads (0.3 mm, 0.19 mm) is refused, naming the least width that scans. An empty field draws nothing.\n\nA record\'s **photos** (a files field, an act\'s paper hands each as a file cell `{id, name, mime, url}`) print too, and only for a file whoever makes the paper may read \u2014 the same rule the file proxy applies; any other file is never drawn. html: `{{#each photos}}<img src="{{url}}">{{/each}}` (only the data\'s file cells are inlined; a url typed into a text field stays a link). Word: `{{IMAGE picture(cover, 8)}}`, and `{{FOR p IN photos}}{{IMAGE picture($p, 8)}}{{END-FOR p}}` for several. Excel: `{{cover | picture:6}}` as the whole cell, or `{{this | picture}}` inside `{{#each photos}}` (each row grows to its picture). The number is the width in centimetres (8, or 6 in Excel, by default). `picture` takes one image file: a list (loop over it), a file that is no image or one that cannot be read is refused by name. Photos are embedded at most 1600 pixels across, about 200 dpi at 20 cm.\n\nA Word command (`{{ \u2026 }}`) reads the data and calls, by name, a Word helper (`picture`, `qr`, `code128`) or one of these text\nand number functions: `trim`, `trimStart`, `trimEnd`, `lower`, `upper`, `capitalize`, `replace`, `replaceAll`, `removeChars`, `removeSpaces`,\n`startsWith`, `endsWith`, `contains`, `substring`, `padStart`, `padEnd`, `length`, `join`, `split`, `repeat`, `formatNumber`,\n`formatDecimal`, `numberToWords`. It holds no JavaScript method or regular-expression literal: `{{upper(name)}}`,\n`{{formatDecimal(total, 0, "vi-VN")}}`, and `{{removeSpaces(removeChars(cccd || "", ".\u2026"))}}` for the text without its dots,\nellipses and spaces. `removeChars(text, chars)` removes every character of `text` that occurs in `chars` (a set of characters,\nnever a pattern); `removeSpaces(text)` removes every whitespace character of `text`, anywhere in it, the set `trim` trims.\n\nA field named like a function is read as the field, and calling the function then fails.\n\n`set_template`\'s description holds each type\'s exact marker grammar and helper list.\n\n## Generating a filled file\n\n```\nget_template {"template_id": "dtl_\u2026"} # its declared variables\ngenerate_document {"document_template_id": "dtl_\u2026", "filename": "invoice-1042",\n "data": {"company_name": "Acme", "total": 1042, "items": [...]}}\n```\n\n- `data` is a map keyed by your variable names. Scalars for scalar/form fields; arrays for\n `table`/`list`/loop variables. `filename` excludes the extension (the type sets it). Pass a select, a\n member or a link as `get_record`\'s `display` words, a number and a date raw.\n- `generate_document` renders the template\'s own type and returns a generated file object; take\n its `file_id` onward.\n- A PDF no template is wanted for takes `html`, a `filename` and its `data` in place of `document_template_id`: the markup is\n rendered as an `html` template is (the same markup rules, markers and helpers) and the PDF is stored as a workspace file. Name one of the two.\n- In a workflow, a **generate step** calls the same tool and hands the `file_id` to the next\n step (attach to a record, send as an attachment, etc.).\n\n## A template an app\'s Act makes\n\nAn Act with `template` (or `templates`) fills an `html`, `excel` or `word` template from its record and files the result\n(a PDF, an `.xlsx` or a `.docx`) in the record\'s files field `into`; `run_app_workflow` on `act_<key>` (or the `workflow` the Act names) presses it. The Act hands the template the record\'s fields by their\nalias, so name each variable after one: `{{customer}}`, `{{total}}`. A select, a member and a link arrive as their words\n(a link as the linked row\'s title); a number and a date arrive raw, for `{{money total "VND"}}` and `{{date issued_on}}`.\nThe rows of each child the record draws arrive under the name of the record\'s own link to them, for a loop:\n\n```html\n<h1>Quote {{code}}</h1>\n<p>{{customer}} \u2014 {{money total "VND"}}</p>\n{{#each lines}}<p>{{description}} \xD7 {{quantity}}</p>{{/each}}\n```\n\nHere `code`, `customer` and `total` are fields of the record and `lines` is its link to the child table. The rows\nalso arrive under the child table\'s `ref` (`quote_line`), so `{{#each quote_line}}` fills as well; a Word template loops the same rows with `{{FOR line IN lines}}` \u2026\n`{{$line.description}}` \u2026 `{{END-FOR line}}`.\n\nEvery List the record draws and the template names (an html template\'s variables, a Word template\'s markers) hands its\nrows, each List\'s `where` applied; a List the template does not name is not read. A List that only reads hands them under its\n`key` (the table\'s alias where it states none), and a List that sums a number hands the sums under `<name>_totals`, one\nentry per number the List draws as a column or as the table\'s figure. A `where` bound may be `{ "record": "<field>" }`,\na date or number the record itself holds, so a record can print a window of its own days. A service plan row (customer,\n`period_start`, `period_end`) draws the customer\'s visits within its period:\n\n```\n{ "type": "List", "key": "visits", "entity": "visit", "same": "customer", "label": "Visits", "columns": ["visited_on", "hours"],\n "where": { "visited_on": { "gte": { "record": "period_start" }, "lte": { "record": "period_end" } } } }\n```\n\n```html\n{{#each visits}}<p>{{visited_on}} \u2014 {{hours}} h</p>{{/each}}\n<p>Total {{visits_totals.hours}} h</p>\n```\n\nA bound the record leaves blank, or holds as anything but one whole day (`2026-05`) or one figure, keeps no row:\n`visits` is empty and `visits_totals.hours` is `0`. The sums add a number as the List\'s foot does: a cell holding a\nfigure adds it, any other cell (blank, an errored formula, a lookup\'s list) adds nothing. A Word template loops\n`{{FOR v IN visits}}` and prints `{{visits_totals.hours}}` the same way. A template that names a List narrowed to the\nreader (`"me"`) is refused, and so is a name a rows List and a `list` List both reach: give the `list` a `key`. A record\nbound narrows what is listed and is never an access bound; the table\'s row rules are.\n\nA paper over many rows (a client\'s month, a statement) is that: a record of the period whose List is windowed by its dates, made by the Act\'s `template`; or a List `Export` with `template` and `per` (`reference/export-node`).\n`reference/groups-and-templates` has the rest.\n\n## Discovering and managing templates\n\nThese tools work across all five types:\n\n| Tool | Does |\n|---|---|\n| `get_workspace` | list the workspace\'s templates (`kind: "template"`), each with its id, type and one line |\n| `get_template` | one template\'s full definition, incl. the `variables` it expects and the `current_version` a change needs |\n| `set_template` | create a template, or change one |\n| `clone_template` | copy a template to iterate on, in chat |\n| `delete_template` | remove a template |\n\nCall `get_template` before generating when you don\'t already know a template\'s variable names.\n';
1802
1802
 
1803
1803
  // docs/knowledge_docs.md
1804
1804
  var knowledge_docs_default = '# Knowledge Docs\n\nKnowledge docs are the workspace\'s **rulebook layer** \u2014 the facts and rules an agent can\'t know on its own: a\nprocedure, a policy, a glossary of in-house terms, a tariff book. You write them once; the agent **searches and reads\nthem on demand** while it works, pulling in only the lines it needs. They are not injected wholesale, so a 10,000-line\ntariff costs nothing until a question touches it.\n\nA menu with its prices, the customer list and the open orders are not knowledge: they are rows of tables, so apps, queries and documents\ncan use them.\n\n## Rulebook, not methodology\n\nPut in a knowledge doc the things the model **cannot guess**: your specific numbers, codes, names, exceptions and\npolicies. Do **not** put in it general skills the model already has ("how to write a polite email"). If the agent would\nget it right without the doc, the doc is noise.\n\n## Writing a doc \u2014 `set_knowledge`\n\n`set_knowledge` creates a doc when it is given no `knowledge_doc_id` and changes the named one when it is given one:\n\n- `name` \u2014 what it is. Just the name: **grouping belongs in `tags`, not in a prefix baked into every title.**\n- `description` \u2014 what the doc is **and what to grep it for**: its vocabulary and the synonyms an ambiguous query would\n use. Surfaced in the listing before any body is read, and truncated at 200 characters.\n- `content` \u2014 Markdown.\n- `tags` *(optional)* \u2014 the labels someone would filter by, e.g. `["H\u1ED3 s\u01A1 NOXH", "Kho\u1EA3n 13"]`. Give **every** one that is\n true of the doc: a corpus is narrowed by intersecting tags, so a doc carrying one label is found down one path only.\n On a change the array **replaces** what is stored, so resend the labels it should keep.\n- `files` *(optional)* \u2014 file ids of the source `.docx`/`.pdf`/`.xlsx`, stored alongside so a reader can open the original.\n\nA change sends only the fields it changes. `edits` (anchored replace, insert, append) makes a surgical change to the\nbody. Refine structure as you learn what users ask: add the synonym that failed to match, split a line that was too\ncoarse, restate a term the heading was carrying.\n\nA new doc is **owned by you and private**. `set_access` shares it: `resources[]` with `resource_type` `knowledge_doc`, and `principals[]` (a\nmember, a member group, or `organization` for everyone), each with a `role` of `user` or `manager`; a principal without a `role` has\nits access taken away. A doc reaches an agent only when the member can `use` it, an organization admin included.\n\n## How an agent uses a doc\n\nRetrieval works like a filesystem, and respects access: only docs the caller may use ever surface.\n\n1. **List and search** \u2014 `get_workspace` kind `knowledge` lists the corpus (id, name, tags, a truncated\n description); a search term matches a doc\'s name, description and tags. `get_workspace` with `search` finds where something is said: it\n matches across every readable doc and returns the **doc, line number and matching line**;\n `knowledge_doc_id` or `tag` confines the lines to one doc or to the docs carrying that tag.\n2. **Read** \u2014 `read_knowledge`, a doc whole or by line range, to see the context around a hit.\n\nMatching is **not ranked**: the *agent* does the narrowing. A distinctive phrase is sharply selective, a whole question\nmatches everything. A search always reports `total_matches`, so "too broad" is visible and cheap to fix.\n\nThe `search` is **literal text** \u2014 every character matches itself. Set `regex: true` to read it as a POSIX regular\nexpression (a word boundary is `\\y`, not `\\b`):\n\n```\n{ "search": "C/O (Form E)", "kind": "knowledge" }\n{ "search": "\\\\yNK\\\\y", "kind": "knowledge", "regex": true, "case_sensitive": true }\n```\n\nAs an expression `C/O (Form E)` would search for `C/O Form E`, which is not in the document, and return a confident\nzero; the literal default has no such failure. With `regex` omitted, a literal search that finds nothing while the\npattern, read as an expression, matches (`a|b`, the commonest) returns those lines under a note saying how they were\nread. `regex: false` keeps the literal zero; a search that finds nothing either\nway says so.\n\n- **Tone marks match exactly by default.** `diacritic_insensitive: true` folds them (`ca phe` matches `c\xE0 ph\xEA`); use it\n when the pattern was typed without them, since folding lets a short word match inside unrelated ones.\n- **Case folds by default.** `case_sensitive: true` matches exactly, for an acronym (`NK` vs `nk`).\n- **Whitespace is normalized on both sides.** Non-breaking spaces, soft hyphens and padded runs from PDF, Word or Excel\n bodies fold to single spaces before matching.\n\nA search shows at most 16,000 characters, shared by what it finds in knowledge, in structure and in records. Matched\nlines are admitted first and context fills what remains; anything cut is reported, with the true number of matching\nlines. Lines longer than 600 characters are clipped and marked; `read_knowledge` gives the rest.\n\n## Write for grep\n\nRetrieval addresses **lines**. Every rule follows from that one fact.\n\n- **One self-contained fact per line.** A hit returns *that line*. For dense or tabular data \u2014 a price row, a tariff\n code \u2014 put the whole record on one line.\n- **Repeat the searchable terms on every line; headings do not carry down.** A line reading `Rate: 15%` under a heading\n `Roasted coffee` will never match a search for `coffee`. Restate the identifying terms inline, even when it reads\n redundantly to a human. This is the rule most often got wrong.\n- **Keep a line under ~600 characters,** the identifying terms early and the long tail late.\n- **Put synonyms and translations on the line itself.** A bilingual row (`C\xE0 ph\xEA, \u0111\xE3 rang / Coffee, roasted`) matches\n queries in either language.\n- **For prose, keep a rule and its exception close.** A rule on line 40 and its exception on line 90 can be retrieved apart.\n\nMarkdown headings carry no retrieval weight of their own; a doc\'s `##` headings outside its blocks, two or more, are\nlisted on its page as its sections.\n\nThe **description is the discovery hint**: it is the only clue for *what term to grep for* before a body is read. Name\nthe doc\'s vocabulary in a sentence.\n\n## Classifying a corpus \u2014 tags\n\nA doc carries a SET of labels, not a folder. Order does not matter, a doc can wear several, and the vocabulary is\nwhatever the docs themselves use. `tags` on `set_knowledge` states one doc\'s complete set.\n\n## Deleting and restoring\n\n`delete_knowledge` deletes docs: they leave every listing, search and read. **Their sharing and version history are\nkept**, so a deleted doc is recoverable. `get_workspace` kind `knowledge` with `include_archived: true` lists the deleted\ndocs you manage, and `restore_knowledge` brings them back with the access they had. Both take many ids and are all or\nnone: every id must be a doc you manage, or nothing changes. A doc with no owner is restored only by an admin, who\nbecomes its owner. A doc that an app or workflow still names cannot be deleted.\n\n## Formatting \u2014 what a doc draws\n\nA doc is markdown with GFM tables. Beyond that it draws a few blocks, each in syntax you already write; the platform draws them in its own style, so every doc looks alike.\n\n| Write | Draws |\n|---|---|\n| `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` opening a blockquote | a callout in that tone |\n| `<Note>`, `<Info>`, `<Tip>`, `<Check>`, `<Warning>`, `<Danger>` | the same callouts |\n| a ```mermaid fence | a diagram: flowchart, sequence, timeline, gantt, `pie`, `xychart-beta`, `quadrantChart` (a 2x2; quadrant-1, top right, is drawn as the target), in the platform\'s theme; a flowchart\'s or a 2x2\'s own colour statements are dropped |\n| `<Steps>` holding `<Step title="\u2026">` | a numbered walk |\n| `<Tabs>` holding `<Tab title="\u2026">` | switchable panels |\n| `<Columns cols={2}>` (or `<CardGroup>`) holding `<Card title="\u2026" href="\u2026" icon="\u2026">` | side-by-side cards that stack on a phone; `icon` is a glyph from the model reference (a select option\'s mark), and a name it lacks is refused with the nearest |\n| a table whose header cells are all empty | a table with no header row (a key/value table) |\n| `<Accordion title="\u2026">`, `<AccordionGroup>`, or `<details>` with a one-line `<summary>\u2026</summary>` | a section that opens on press |\n\nEach tag stands on a line of its own, and a tag line inside a fenced code block is code. Attributes are text in double quotes or a number in braces; nothing in a tag runs. A write that adds a tag that does not close, nests wrongly, carries an attribute the tag does not take, or names a component this list lacks (`<Frame>`, `<CodeGroup>`) is refused with the lines to fix; lowercase HTML (`<br>`) stays text.\n\nA chart\'s numbers live in its mermaid source, which agents read and grep like any text: write them there once, not again in a table.\n';
1805
1805
 
1806
1806
  // docs/migration.md
1807
- var migration_default = "# @lotics/cli \u2014 migration notes\n\nWhat to DO when a release changes the shape of something the CLI once wrote to disk. Every other\ncontract is `docs/cli_reference.md`; this file is the one a reader opens once, because something\nalready on disk no longer matches what the CLI does.\n\n## Local app projects are gone\n\nAn app stated as a tree no longer lives in a directory. The live app is the only edit surface:\neach change mints a new version of it, and applying the earlier tree again is the undo. What\nthe app is \u2014 its screens (`app.json`), its queries, workflows, agents and capabilities, and the\ntree it was compiled from \u2014 is held by that version, and the tables, fields, options, templates and\nroles are the workspace's own.\n\n| What you ran | What replaces it |\n|---|---|\n| `lotics scaffold check` + `scaffold apply`, `lotics workspace build <model.json>`, `lotics setup <file>` | `lotics run set_tables` with the tables of the workspace in one list (at most 50 a call), then `set_app` for each app (`lotics app apply <file>`); `dry_run` checks and writes nothing |\n| `lotics app create --from <model.json>#<app>` | `lotics app apply <file>` \u2014 `set_app` creates the app from its tree |\n| `lotics app regenerate` | change the tree, then `lotics app apply <file>` |\n| `lotics scaffold diff` | `lotics app apply <file> --check` \u2014 what would refuse the tree, writing nothing |\n| `lotics scaffold export` | `lotics app pull <app_id> [path]` \u2014 the app's tree as JSON, one app at a time |\n| `lotics app codegen`, `app check`, `app dev`, `app preview` | nothing local: read an app with `lotics run get_app`, and change it through its tools |\n| `lotics app workflow set` / `app query set` | `lotics run set_app_workflow` / `set_app_queries` \u2014 each mints a version |\n| `lotics app versions` + a redeploy of an old tree | apply the earlier tree again (`lotics app apply <file>`), or deploy the earlier source; table changes and data writes stay |\n| `lotics app rename`, `app subdomain`, `package.json#lotics.capabilities` | `lotics run set_app` with the app's id (`public_subdomain`, `capabilities`; its name is the tree's `name`) |\n| `lotics app api publish` / `unpublish` | `lotics run publish_app_api` / `unpublish_app_api` |\n| `lotics field rename` | `lotics run set_tables` with a merge patch of the table's document |\n| `lotics app kit` | nothing: a custom-code app depends on `@lotics/app-sdk` (below) |\n| `@lotics/xlsx` / `@lotics/docx` to build a template's file | any `.xlsx` / `.docx` library, then `lotics upload` (`lotics docs document_templates`) |\n| `lotics file preview` | `lotics file download <fil_id>`, then open it |\n| `lotics library list` / `library show <slug>`, `lotics preset list` / `preset show <slug>` | the example files at `https://lotics.ai/presets/index.json`, each read and its tables passed to `set_tables` |\n| `lotics library init <apg_id>`, `lotics app upgrade`, `lotics library fixtures` | nothing copies a package any more: state the tables (`lotics docs model`) and pass them to `set_tables` |\n\n`lotics tools` lists every tool, and `lotics tools <name>` prints its input.\n\n**An existing JSON app moves with `lotics app pull <app_id>`**: it writes the app's tree, and\n`lotics app apply` of that file edits the app and mints its next version. A directory an earlier CLI wrote (`app.json`, `src/workflows/`, `.lotics/`, `package.json#lotics`\nwith `plan`, `queries` or `workflows`) is no longer read by anything; keep the pulled tree, delete the\nrest.\n\n**Authored workflow bodies** an act names (`workflow` on an act) are the live workflow's own body:\n`lotics run set_app_workflow` changes it.\n\n**A table written before `party`** draws its people and organisations as things \u2014 a row with no\npicture shows no initials: state `party` (`person` or `organization`) in that table's `records`.\n\n## Custom-code apps use `@lotics/app-sdk`\n\nA hand-written app depends on `@lotics/app-sdk` alone \u2014 the hooks (`useQuery`, `useWorkflow`, \u2026),\n`mount`, and `AppRouter` from `@lotics/app-sdk/router` \u2014 and draws with its own React.\n`@lotics/app-runtime` and `@lotics/ui` are no longer published: the runtime is what the platform\ndraws an app with, and it ships with the platform.\n\nAn app that listed `@lotics/app-runtime`:\n\n1. **The dependency.** `@lotics/app-sdk` replaces `@lotics/app-runtime` in `package.json`.\n2. **The import specifiers.** `@lotics/app-runtime/sdk` becomes `@lotics/app-sdk`, and\n `@lotics/app-runtime/router` becomes `@lotics/app-sdk/router` \u2014 in `src/` and in\n `vite.config.ts`.\n3. **The kit.** An app that imports `@lotics/ui` keeps the version it already installed; no newer one\n is published. Its screens can stay on it, or move to plain React (or any library) one screen at a\n time.\n4. **`package.json#lotics`** needs only `app_id`, `workspace_id` and `current_version_id`. The live\n app owns its `queries`, `workflows`, `agents` and `capabilities`, and `lotics app deploy` refuses\n a directory that still declares any of them; delete them, and the unread `synced` block.\n5. `lotics app deploy` builds the directory and uploads it as a new version.\n\nA deployed app keeps running on the bundle it shipped until it is deployed again.\n";
1807
+ var migration_default = "# @lotics/cli \u2014 migration notes\n\nWhat to DO when a release changes the shape of something the CLI once wrote to disk. Every other\ncontract is `docs/cli_reference.md`; this file is the one a reader opens once, because something\nalready on disk no longer matches what the CLI does.\n\n## Local app projects are gone\n\nAn app stated as a tree no longer lives in a directory. The live app is the only edit surface:\neach change mints a new version of it, and applying the earlier tree again is the undo. What\nthe app is \u2014 its screens (`app.json`), its queries, workflows, agents and capabilities, and the\ntree it was compiled from \u2014 is held by that version, and the tables, fields, options, templates and\nroles are the workspace's own.\n\n| What you ran | What replaces it |\n|---|---|\n| `lotics scaffold check` + `scaffold apply`, `lotics workspace build <model.json>`, `lotics setup <file>` | `lotics run set_tables` with the tables of the workspace in one list (at most 50 a call), then `set_app` for each app (`lotics app apply <file>`); `dry_run` checks and writes nothing |\n| `lotics app create --from <model.json>#<app>` | `lotics app apply <file>` \u2014 `set_app` creates the app from its tree |\n| `lotics app regenerate` | change the tree, then `lotics app apply <file>` |\n| `lotics scaffold diff` | `lotics app apply <file> --check` \u2014 what would refuse the tree, writing nothing |\n| `lotics scaffold export` | `lotics app pull <app_id> [path]` \u2014 the app's tree as JSON, one app at a time |\n| `lotics app codegen`, `app check`, `app dev`, `app preview` | nothing local: read an app with `lotics run get_app`, and change it through its tools |\n| `lotics app workflow set` / `app query set` | `lotics run set_app_workflow` / `set_app_queries` \u2014 each mints a version |\n| `lotics app versions` + a redeploy of an old tree | apply the earlier tree again (`lotics app apply <file>`), or deploy the earlier source; table changes and data writes stay |\n| `lotics app rename`, `app subdomain`, `package.json#lotics.capabilities` | `lotics run set_app` with the app's id (`public_subdomain`, `capabilities`; its name is the tree's `name`) |\n| `lotics app api publish` / `unpublish` | `lotics run publish_app_api` / `unpublish_app_api` |\n| `lotics field rename` | `lotics run set_tables` with a merge patch of the table's document |\n| `lotics app kit` | nothing: a custom-code app depends on `@lotics/app-sdk` (below) |\n| `@lotics/xlsx` / `@lotics/docx` to build a template's file | any `.xlsx` / `.docx` library, then `lotics upload` (`lotics docs document_templates`) |\n| `lotics file preview` | `lotics file download <fil_id>`, then open it |\n| `lotics library list` / `library show <slug>`, `lotics preset list` / `preset show <slug>` | the example files at `https://lotics.ai/presets/index.json`, each read and its tables passed to `set_tables` |\n| `lotics library init <apg_id>`, `lotics app upgrade`, `lotics library fixtures` | nothing copies a package any more: state the tables (`lotics docs reference`) and pass them to `set_tables` |\n\n`lotics tools` lists every tool, and `lotics tools <name>` prints its input.\n\n**An existing JSON app moves with `lotics app pull <app_id>`**: it writes the app's tree, and\n`lotics app apply` of that file edits the app and mints its next version. A directory an earlier CLI wrote (`app.json`, `src/workflows/`, `.lotics/`, `package.json#lotics`\nwith `plan`, `queries` or `workflows`) is no longer read by anything; keep the pulled tree, delete the\nrest.\n\n**Authored workflow bodies** an act names (`workflow` on an act) are the live workflow's own body:\n`lotics run set_app_workflow` changes it.\n\n**A table written before `party`** draws its people and organisations as things \u2014 a row with no\npicture shows no initials: state `party` (`person` or `organization`) in that table's `records`.\n\n## Custom-code apps use `@lotics/app-sdk`\n\nA hand-written app depends on `@lotics/app-sdk` alone \u2014 the hooks (`useQuery`, `useWorkflow`, \u2026),\n`mount`, and `AppRouter` from `@lotics/app-sdk/router` \u2014 and draws with its own React.\n`@lotics/app-runtime` and `@lotics/ui` are no longer published: the runtime is what the platform\ndraws an app with, and it ships with the platform.\n\nAn app that listed `@lotics/app-runtime`:\n\n1. **The dependency.** `@lotics/app-sdk` replaces `@lotics/app-runtime` in `package.json`.\n2. **The import specifiers.** `@lotics/app-runtime/sdk` becomes `@lotics/app-sdk`, and\n `@lotics/app-runtime/router` becomes `@lotics/app-sdk/router` \u2014 in `src/` and in\n `vite.config.ts`.\n3. **The kit.** An app that imports `@lotics/ui` keeps the version it already installed; no newer one\n is published. Its screens can stay on it, or move to plain React (or any library) one screen at a\n time.\n4. **`package.json#lotics`** needs only `app_id`, `workspace_id` and `current_version_id`. The live\n app owns its `queries`, `workflows`, `agents` and `capabilities`, and `lotics app deploy` refuses\n a directory that still declares any of them; delete them, and the unread `synced` block.\n5. `lotics app deploy` builds the directory and uploads it as a new version.\n\nA deployed app keeps running on the bundle it shipped until it is deployed again.\n";
1808
1808
 
1809
1809
  // ../shared/src/doc_sources.ts
1810
1810
  var JOBS = [
@@ -1821,7 +1821,7 @@ var JOBS = [
1821
1821
  { area: "building_an_app", file: "docs/building_an_app.md", covers: "the order the steps go in, and why", readers: ["cli"] },
1822
1822
  { area: "app_architecture", file: "docs/app_architecture.md", covers: "one factory's split into apps: the job, person, register and contents of each", readers: ["cli", "tool"] },
1823
1823
  { area: "design", file: "docs/design.md", covers: "the treatment drawing each thing a job reads, the visual bar, looking at what a write built", readers: ["cli", "tool"] },
1824
- { area: "model", file: "src/model_reference.md", covers: "every key of a table document and an app body: tables, fields, write rules, records, apps", readers: ["cli", "tool"] },
1824
+ { area: "reference", file: "src/reference.md", covers: "every key of a table document and an app body: tables, fields, write rules, records, apps", readers: ["cli", "tool"] },
1825
1825
  { area: "examples", file: "docs/examples.md", covers: "each treatment worked through in a complete set of tables and apps", readers: ["cli", "tool"] },
1826
1826
  { area: "agent_surface", file: "docs/agent_surface.md", covers: "the queries and workflows every app generates, how an agent calls them, the file an app's Export makes, and declaring more", readers: ["cli", "tool"] }
1827
1827
  ]
@@ -2208,7 +2208,7 @@ var TEXTS = {
2208
2208
  "docs/design.md": design_default,
2209
2209
  "docs/app_architecture.md": app_architecture_default,
2210
2210
  "docs/working_in_a_workspace.md": working_in_a_workspace_default,
2211
- "src/model_reference.md": model_reference_default,
2211
+ "src/reference.md": reference_default,
2212
2212
  "docs/examples.md": examples_default,
2213
2213
  "docs/agent_surface.md": agent_surface_default,
2214
2214
  "AGENTS.md": AGENTS_default,
@@ -2227,7 +2227,7 @@ var TEXTS = {
2227
2227
  var DOC_AREAS = docSourcesFor("cli").map(({ area, job, covers, file: file2 }) => ({ area, job, covers, text: TEXTS[file2] }));
2228
2228
  var CLI_SURFACE = {
2229
2229
  usage: [
2230
- " lotics docs <area> print one, e.g. `lotics docs model`",
2230
+ " lotics docs <area> print one, e.g. `lotics docs reference`",
2231
2231
  " lotics docs <area>/<section> one section \u2014 a long doc prints its section list",
2232
2232
  " lotics docs [<area>] --grep <text> the pages that say it, in the references the server serves",
2233
2233
  " A custom-code app's SDK reference is node_modules/@lotics/app-sdk/AGENTS.md in the app."
@@ -2338,10 +2338,10 @@ var COMMANDS = [
2338
2338
  flags: { docs: ["--grep", "--regex", "--literal", "--case-sensitive", "--diacritic-insensitive", "--context-lines", "--limit"] },
2339
2339
  help: [
2340
2340
  " lotics docs List this CLI's reference docs",
2341
- " lotics docs model The reference for tables and apps, from inside this CLI:",
2341
+ " lotics docs reference The reference for tables and apps, from inside this CLI:",
2342
2342
  " every key, field type, act and check, and a worked",
2343
2343
  " example. Offline, no account",
2344
- " lotics docs <area> Print one, capped at a page (e.g. 'lotics docs model')",
2344
+ " lotics docs <area> Print one, capped at a page (e.g. 'lotics docs reference')",
2345
2345
  " lotics docs <area>/<section> One section, or one row of a table doc",
2346
2346
  " lotics docs [<area>] --grep <text> Search the references on the server (--regex,",
2347
2347
  " --literal, --case-sensitive, --diacritic-insensitive,",
@@ -26849,7 +26849,7 @@ var contractEntitySchema = zod_default.object({
26849
26849
  * `label` is the TABLE's name and names the SET it holds, so a register's own
26850
26850
  * act reads "New Customers" in a language that inflects the plural. Optional
26851
26851
  * because a table no create opens needs none; one a create opens is refused
26852
- * without it (`entity.singular`, `app_model_validate_singular.ts`).
26852
+ * without it (`entity.singular`, `tree_check/writes.ts`).
26853
26853
  */
26854
26854
  singular: zod_default.string().min(1).optional().describe("One row of this table, in the business's own words \u2014 what a create's button and panel name"),
26855
26855
  description: zod_default.string().optional().describe("The table's description, written onto the table in the workspace, where a caller of an app's queries and workflows reads it beside them"),
@@ -26892,6 +26892,11 @@ var wordTemplate = uploadedTemplate("word", ".docx").extend({
26892
26892
  markers: wordMarkersSchema.optional().describe("The markers the document holds, as the workspace stores them; absent where a contract predates them")
26893
26893
  });
26894
26894
  var contractTemplateSchema = zod_default.discriminatedUnion("type", [inlineTemplate("html"), inlineTemplate("email"), uploadedTemplate("excel", ".xlsx"), wordTemplate]).describe("html is a page a workflow renders to a PDF; email is a message a workflow sends; excel is a workbook a register's export or an act fills; word is a document an act fills");
26895
+ var appModelContractSchema = zod_default.object({
26896
+ entities: zod_default.array(contractEntitySchema),
26897
+ roles: zod_default.array(contractRoleSchema).default([]),
26898
+ templates: zod_default.array(contractTemplateSchema).default([])
26899
+ }).strict();
26895
26900
  var contractFixtureRowSchema = zod_default.object({
26896
26901
  /**
26897
26902
  * Stable, per-entity handle another row's link field addresses as
@@ -26906,40 +26911,40 @@ var FIXTURE_ROWS_MAX = 200;
26906
26911
  // ../shared/src/schemas/social_networks.ts
26907
26912
  var SOCIAL_NETWORKS = ["facebook", "instagram", "threads", "x", "linkedin"];
26908
26913
 
26909
- // ../shared/src/schemas/model_rules.ts
26914
+ // ../shared/src/schemas/app_rules.ts
26910
26915
  var ENTITIES_MAX = 50;
26911
26916
  var MODEL_ROWS_MAX = 2e3;
26912
26917
  var MODEL_DOCUMENTS_MAX = 2e3;
26913
- var MODEL_RULES = {
26914
- // ─── The model ───────────────────────────────────────────────────────────────
26915
- "model.schema": { sentence: "Every key is one its table lists, holding the type its row gives, and every required key is stated." },
26916
- "model.names-declared": {
26918
+ var APP_RULES = {
26919
+ // ─── The tree and its tables ───────────────────────────────────────────────────────────────
26920
+ "app.schema": { sentence: "Every key is one its table lists, holding the type its row gives, and every required key is stated." },
26921
+ "app.names-declared": {
26917
26922
  sentence: "Every alias a key names is declared: a table of the App's `tables`, a field of the table the node reads, an option of the select it names, an Act `key` of the App, a template of its `templates`, or an app."
26918
26923
  },
26919
- "model.tables": { sentence: `A call declares at least one table and at most ${ENTITIES_MAX}; send the rest in a second call.` },
26920
- "model.rows-cap": {
26924
+ "app.tables": { sentence: `A call declares at least one table and at most ${ENTITIES_MAX}; send the rest in a second call.` },
26925
+ "app.rows-cap": {
26921
26926
  sentence: `Rows are a sample: at most ${FIXTURE_ROWS_MAX} per entity, ${MODEL_ROWS_MAX} per call and ${MODEL_DOCUMENTS_MAX} documents attached \u2014 a real data set goes in with \`create_records\` once the tables exist.`
26922
26927
  },
26923
- "model.alias-unique": { sentence: "An alias is unique where it is named: tables, fields and views in their table, options in their select." },
26924
- "model.label-unique": {
26928
+ "app.alias-scope": { sentence: "An alias is unique where it is named: tables, fields and views in their table, options in their select." },
26929
+ "app.label-unique": {
26925
26930
  sentence: "A label is unique where it is found by its label: fields and views in their table, options in their select \u2014 and no view takes its table's own label, which names the whole-table grid."
26926
26931
  },
26927
- "model.template-kind": { sentence: "An Act's `template` is an html, excel or word document template and an Export's `template` an html or excel one, never an email or pdf-form one." },
26928
- "model.template-marker": {
26932
+ "template.kind": { sentence: "An Act's `template` is an html, excel or word document template and an Export's `template` an html or excel one, never an email or pdf-form one." },
26933
+ "template.marker": {
26929
26934
  sentence: "Every marker of a Word template an Act makes is filled by that Act: a field of its entity, or a list of the child rows its record lists (the entity's own alias over several rows)."
26930
26935
  },
26931
- "model.template-act": {
26936
+ "template.act": {
26932
26937
  sentence: "In an App none of whose Acts runs an authored `workflow`, every html, excel or word template is made by an Act (`template`, `templates`) or filled by an Export \u2014 the note names the app over the table whose fields it prints most, where the Act belongs.",
26933
26938
  note: true,
26934
26939
  design: "templates"
26935
26940
  },
26936
- "register.export-report": {
26941
+ "list.export-report": {
26937
26942
  sentence: "A List's Exports each have their own `label`; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes."
26938
26943
  },
26939
- "register.export-template": {
26944
+ "list.export-template": {
26940
26945
  sentence: "A template an Export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the List's entity alias \u2014 an entity named none of those \u2014 and is the document of no Act."
26941
26946
  },
26942
- "model.filter": {
26947
+ "app.filter": {
26943
26948
  sentence: "A filter \u2014 a view's, a rollup's \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares."
26944
26949
  },
26945
26950
  // ─── Entities ────────────────────────────────────────────────────────────────
@@ -27077,9 +27082,6 @@ var MODEL_RULES = {
27077
27082
  sentence: "A many-link to rows that each belong to one row of this entity is never a Field of a part, a `create` question or an Act's `asks`: those rows are the record's own, read as a child List."
27078
27083
  },
27079
27084
  "app.create-asks": { sentence: "A List's `create` asks fields a person writes \u2014 never the link or the folder it files the row under, which the add sets itself." },
27080
- "app.edits-entity": { sentence: "A stored body's `edits` name an entity the app reaches: its own, a log's, a child its record lists, a row a link opens or a document it makes.", stored: true },
27081
- "app.edits-field": { sentence: "A stored body's `edits` of an entity name its stored fields \u2014 not a formula, rollup, lookup or autonumber \u2014 once each.", stored: true },
27082
- "app.edits-owned": { sentence: "A stored body's `edits` never name a field one of its own Acts changes (`set`, `sign`, `writes`, `asks`, `into`, a stage's clock): an Act's outcome is no save's to change.", stored: true },
27083
27085
  "app.create-required": {
27084
27086
  sentence: "A List's `create` asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add."
27085
27087
  },
@@ -27097,11 +27099,6 @@ var MODEL_RULES = {
27097
27099
  "app.logs": {
27098
27100
  sentence: `A register App's logs \u2014 further Lists, at most ${APP_LOGS_MAX} \u2014 each list another entity under a \`key\` of their own: never the App's own entity, one of its Books' or another log's. A log is one List \u2014 a table or cards, with no Tabs, no Book \`column\` and no composed Record \u2014 over an entity whose status states no owned \`stages\`, and stands in an App that states no \`scope\`. An Act's \`key\` is its own among the App's Acts, its logs' included. No Record of the App or of a log lists the rows of an entity another of the App's Lists is over: a link to one opens it on its own List. A List people work as a desk of its own is another App.`
27099
27101
  },
27100
- "app.rates-in-passing": {
27101
- sentence: "An App with no Act, log or Tabs of its own whose rows another App's lines copy a number off (`default_from` through a link) \u2014 the rates a quote is priced from \u2014 is worked only while that record is written: a child List, a Section or an Act of that App, never an App of its own.",
27102
- note: true,
27103
- stored: true
27104
- },
27105
27102
  "app.row": { sentence: "A List's `row` states only what differs from `records`: a `subtitle` that is not the title, a number for its `figure`, dates for its `due`, an `image` that is a files picture no Act makes documents into, or `false` for a figure, deadlines or picture the entity states and the App draws none of." },
27106
27103
  "app.tasks": {
27107
27104
  sentence: "A task entity an App lists whose rows no write of the App moves to a closed option (its ring stands disabled), or that an Act ticks done and none moves back.",
@@ -27130,106 +27127,102 @@ var MODEL_RULES = {
27130
27127
  "app.picker-link": { sentence: "`add`, `record` and `remove` stand on a Field that is a link: only a link has a picker and a card." },
27131
27128
  "app.add-filed": { sentence: "The Lists adding rows of one table file them by one link, the first such List's: a List over that table filed by another link lists its rows and adds none." },
27132
27129
  "app.default-breaks": { sentence: "A node the platform draws where the tree states none leaves the App refusing nothing more: where it would, the tree states that node itself." },
27133
- "app.declines": {
27134
- sentence: "A stored body's `declines` names decided rules that fire on the app, each with why it stays as it is: by id, and `records.picture:<entity>`, said once per entity, with what it fires on.",
27135
- stored: true
27136
- },
27137
27130
  // ─── Registers ───────────────────────────────────────────────────────────────
27138
- "register.column-drawn": {
27139
- sentence: "A List's `columns` never name what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure's meter reads against \u2014 and name a field once."
27131
+ "list.column-drawn": {
27132
+ sentence: "A List's `columns` never name what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure's meter reads against \u2014 and name a field once; a child List's never name its link back either, and an `expect` List's figure is its filled-in-place column."
27140
27133
  },
27141
- "register.columns-budget": {
27134
+ "list.columns-budget": {
27142
27135
  sentence: "A List's `columns`, each at the width of its kind, fit the room a desk leaves beside its name and the row's own status, deadline and figure.",
27143
27136
  authoring: true
27144
27137
  },
27145
- "register.filter": {
27138
+ "list.filter": {
27146
27139
  sentence: "A List's `filters` name a select, a member, a link, a date, a yes/no or a number of the List's own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes."
27147
27140
  },
27148
- "register.filter-split": {
27141
+ "list.filter-split": {
27149
27142
  sentence: "A `filters` entry on a field a Reading of the List with `by` or `across` splits the rows by \u2014 a press on its part narrows by it too; a member's filter, which narrows to the reader's own rows, is never noted.",
27150
27143
  note: true
27151
27144
  },
27152
- "register.filter-unshown": {
27145
+ "list.filter-unshown": {
27153
27146
  sentence: "A `filters` entry, a narrowing the List `opens` on, and a press in its band (a Reading's part, a grid's cell, a figure's `where`) name a field its rows show \u2014 a `columns` entry; the row's title, the line under it, its picture, status or figure, a meter's bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under \u2014 or a formula, drawn where every field of its own row it reads is (through other formulas too; one reading no field is drawn nowhere). A narrowing to the reader (`{ \"me\": true }`) needs none. A tree leaves out a `filters` entry no row shows and says so (name the field as a column or in the row's line to offer it); the other two are refused."
27154
27147
  },
27155
- "register.tiers": { sentence: "`tiers` band a number read in one unit on every row; a figure read in each row's own unit or currency is named bare." },
27156
- "register.search": { sentence: "A List's `search` names text, a code, or a link \u2014 matched by its row's title." },
27157
- "register.sort": { sentence: "Each `sort` key of a List or a Tab names a field of the List's rows that holds an order \u2014 never files \u2014 and no field twice." },
27158
- "register.tabs": {
27148
+ "list.tiers": { sentence: "`tiers` band a number read in one unit on every row; a figure read in each row's own unit or currency is named bare." },
27149
+ "list.search": { sentence: "A List's `search` names text, a code, or a link \u2014 matched by its row's title." },
27150
+ "list.sort": { sentence: "Each `sort` key of a List or a Tab names a field of the List's rows that holds an order \u2014 never files \u2014 and no field twice." },
27151
+ "list.tabs": {
27159
27152
  sentence: "A List's Tabs each have a `key` of their own, none the entity's alias or one a report template holds (`title`, `lines`, `readings`, `at`, `dates`, `period`, `per`); a Tab states one of `dated` and `open` (dates of its rows, `open`'s two different) or `totals` (a child's rows windowed by the period); its `where` and `sort` hold as the List's do, and never narrow by the status, which its chips split. Only a table or cards List has Tabs, and a Reading's `tab` names one of them."
27160
27153
  },
27161
- "register.tab-totals": {
27154
+ "list.tab-totals": {
27162
27155
  sentence: "A Tab's `totals` name a child linked to the List's entity by a one-row link (a child linking the List's entity twice cannot be totalled), a date of the child, never a moment or a span, and 1 to 6 columns each under its own alias and label; a column's `value` is a number of the child, its `where` holds over the child's selects and yes/nos, and a value read in each row's own unit or currency stands only where that select of the child looks up, through its link, a single select of the List's entity, and the child links to no Book's rows. A Book's rows are totalled by the child's one link to them, by one field to one row; a Book the child links to not at all totals nothing on its rows (`app.book-lacks`). Only a table List has them, and no Tab states `totals` beside `dated` or `open`."
27163
27156
  },
27164
- "register.group": {
27157
+ "list.group": {
27165
27158
  sentence: "A List's `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 the status too, but never on a task list's block, whose chips narrow it; only a table or cards List groups, and a gantt into its lanes; grouping by the title needs a `row.subtitle` to lead each row."
27166
27159
  },
27167
- "register.readings-own": { sentence: "A Reading in a List reads the List's own entity; another entity's stands on a dashboard." },
27168
- "register.readings-bands": {
27160
+ "list.readings-own": { sentence: "A Reading in a List reads the List's own entity; another entity's stands on a dashboard." },
27161
+ "list.readings-bands": {
27169
27162
  sentence: "A Reading in a List that bands a number by `tiers` has that field with the same tiers among the List's `filters`: a press on a band narrows the rows by that filter."
27170
27163
  },
27171
- "register.readings-restate": {
27164
+ "list.readings-restate": {
27172
27165
  sentence: "A Reading with no `by` in a List never sums the row's figure, counts the rows, or counts one stage's rows, unless it is placed in time (`over`): the summary line totals the figure and the status chips count the rows, but neither draws a change against the last period."
27173
27166
  },
27174
- "register.where-opens": {
27167
+ "list.where-opens": {
27175
27168
  sentence: "A List's `where` holds its own selects, yes/nos, bounds and blanks (a child List's or the register's), and `opens` its selects and yes/nos and a member field holding the reader (`{ \"me\": true }`) \u2014 or, on a status whose `stages` have owners, the rows waiting on the reader; a field `where` fixes to options is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. A List laid out as a `plan` keeps its items by options and yes/nos alone."
27176
27169
  },
27177
- "register.roster": {
27170
+ "list.roster": {
27178
27171
  sentence: "A List with `layout: \"roster\"` holds the List of its cells: the cells' entity has one single link to this one and a date on its line (a field of the row itself, or a lookup of its parent's date), a roster draws no `columns`, and an `expect` (with `of`) stands only on the cells List."
27179
27172
  },
27180
- "register.roster-expect": { sentence: "A roster cells List's `expect` is a single select of its rows, and its `of` this entity's multi-select whose every option is one of `expect`'s." },
27181
- "register.gantt": {
27173
+ "list.roster-expect": { sentence: "A roster cells List's `expect` is a single select of its rows, and its `of` this entity's multi-select whose every option is one of `expect`'s." },
27174
+ "list.gantt": {
27182
27175
  sentence: "`layout: { gantt }`: `start` and `end` (absent, the entity's first `due`) are two different dates of the row, both days or both moments; each diamond (`milestones`) another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity."
27183
27176
  },
27184
- "register.lanes": {
27177
+ "list.lanes": {
27185
27178
  sentence: "`layout: { lanes }`: `by` is a one-row link to an entity with a `records` entry, the rows name a date on their line (a field of the row itself, or a lookup of its parent's date), a lane block's two ends are read alike (two days or two moments), and `loads` stands with it."
27186
27179
  },
27187
- "register.plan": {
27180
+ "list.plan": {
27188
27181
  sentence: "`layout: { plan }`: `at` is a one-row link to an entity whose `records` states a `plan_place`, `across` and `up` two different numbers of the row, `fill` a single select; a plan List has no Tabs, `group` or `attention`. A write that sets `at`, `across` or `up` is refused unless it clears all three, or places the row at a free cell of a bay (a row from 1 to the bay's `across`, a tier from 1 to its `up`, resting on a present item), or at an area with neither."
27189
27182
  },
27190
- "register.plan-unique": {
27183
+ "list.plan-unique": {
27191
27184
  sentence: "A plan's items (the List's table and each Book) state `unique` on their place link, `across` and `up`: the app's moves refuse a taken cell, and the unique makes every other write hold it \u2014 the note names a table that states none.",
27192
27185
  note: true
27193
27186
  },
27194
- "register.plan-mark": {
27187
+ "list.plan-mark": {
27195
27188
  sentence: "A plan's `mark` names a single select of the row and an option of it by alias: a row holding that option says its label on its box and is hatched in the option's colour, which the legend names by its label. A Book that holds the select must hold it as a single select with that option; a Book lacking it never marks its rows (`app.book-lacks`)."
27196
27189
  },
27197
- "register.plan-size": {
27190
+ "list.plan-size": {
27198
27191
  sentence: "A plan's `size` names a single select of the row and, by option alias, the box (`across`, `along`, `up` \u2014 positive, in the one length unit the places' `size` states) of each option: a row holding one is drawn as that box, any other at its place's cell. A Book that holds the select must hold it as a single select; a Book lacking it draws its rows at their places' cells (`app.book-lacks`)."
27199
27192
  },
27200
- "register.plan-box-size": {
27193
+ "list.plan-box-size": {
27201
27194
  sentence: "A plan whose places state a `size` and whose rows' select options are ISO 6346 container size-type codes (`20DC`, `40HC`) states the box of each in `size`, from the ISO 668 dimensions, else every box is drawn at its place's cell.",
27202
27195
  note: true
27203
27196
  },
27204
- "register.loads": {
27197
+ "list.loads": {
27205
27198
  sentence: `A lanes layout's \`loads\` names one to ${LANE_LOADS} numbers a lane block adds \u2014 never a percent \u2014 each against a number of the lane's row, in one unit or two of one dimension.`
27206
27199
  },
27207
- "register.band-pivot": { sentence: "A Reading with `across` (a grid) among a List's Readings: its grid reads on a dashboard, and the band leads with a figure (a Reading with no `by`).", note: true, design: "readings" },
27208
- "register.headline": {
27200
+ "list.band-pivot": { sentence: "A Reading with `across` (a grid) among a List's Readings: its grid reads on a dashboard, and the band leads with a figure (a Reading with no `by`).", note: true, design: "readings" },
27201
+ "list.headline": {
27209
27202
  sentence: "A List over rows that fall due or carry money and have no status whose steps the chips count, laid out as a table, cards or a gantt, has Readings \u2014 they split the rows by a field they show.",
27210
27203
  decide: true,
27211
27204
  design: "readings"
27212
27205
  },
27213
- "register.span": {
27206
+ "list.span": {
27214
27207
  sentence: "Rows that each span a run of days or hours \u2014 two dates on their line, a `frees`, a `no_overlap` booking them on what its `by` links to, or one required date and the table's `due` where the line names no date \u2014 are drawn in time by some List over them (a `layout` of calendar, lanes, gantt or roster) \u2014 the note names the app to add over them, laid out as lanes on what each one books, else as a gantt (a run held by a date and the `due` also asks for both dates on the line).",
27215
27208
  note: true,
27216
27209
  design: "rows-in-time"
27217
27210
  },
27218
- "register.where-bound": { sentence: 'A bound read from the record (`{ "record": \u2026 }`) in `where` narrows only a List that reads its rows \u2014 `same`, `app`, or a Record with `edit: false`.' },
27219
- "register.where-reader": { sentence: 'A List\'s `where` keeps its rows for every reader and never names the reader (`{ "me": true }`): `opens` opens a List on the reader.' },
27220
- "register.props": {
27211
+ "list.where-bound": { sentence: 'A bound read from the record (`{ "record": \u2026 }`) in `where` narrows only a List that reads its rows \u2014 `same`, `app`, or a Record with `edit: false`.' },
27212
+ "list.where-reader": { sentence: 'A List\'s `where` keeps its rows for every reader and never names the reader (`{ "me": true }`): `opens` opens a List on the reader.' },
27213
+ "list.props": {
27221
27214
  sentence: "A node states only the props of its place: a List those of the register, a log, a roster's cells, a rows, agenda, timeline or read-only List of a record, a document or a dashboard; the Record of a List below the register `edit` and its children; a dashboard's Tab its `key`, `label` and children."
27222
27215
  },
27223
- "register.holds": {
27216
+ "list.holds": {
27224
27217
  sentence: "A List holds what its place draws: the register its Tabs, Readings, Progress, Exports, Acts, Checks, Books, Record and, under a roster, the one List of its cells; a child List its Acts, Checks, Record and, with `under`, the List of its documents; a document and a roster's cells their Acts, Checks and Record."
27225
27218
  },
27226
- "register.layout-place": {
27219
+ "list.layout-place": {
27227
27220
  sentence: "The register lays out as a table, cards, a calendar, a roster, a gantt, lanes or a plan; a child List writing its rows as a table, a checklist or a desk; any child List as an agenda or a timeline."
27228
27221
  },
27229
- "register.row-restates": {
27222
+ "list.row-restates": {
27230
27223
  sentence: "A child List and a log read their rows as `records` states them: their `row` states a picker's add or a card on the title or a subtitle, never another `subtitle`, a `figure`, `due`, `image` or `status: false`."
27231
27224
  },
27232
- "register.edit-column": { sentence: 'An edit column (`{ "field": \u2026, "edit": true }`) stands in a child List, which writes its rows in place: the register\'s rows are written where they open.' },
27225
+ "list.edit-column": { sentence: 'An edit column (`{ "field": \u2026, "edit": true }`) stands in a child List, which writes its rows in place: the register\'s rows are written where they open.' },
27233
27226
  // ─── Readings ────────────────────────────────────────────────────────────────
27234
27227
  "reading.value": { sentence: "A Reading sums a number, never a percent \u2014 percents do not add up; a value of `{ avg }` reads one." },
27235
27228
  "reading.average": {
@@ -27271,11 +27264,10 @@ var MODEL_RULES = {
27271
27264
  sentence: "A Field of a part never looks up what its link already reads where it stands: the linked row's title beside the link, the figure or bound its meter reads on the head's line."
27272
27265
  },
27273
27266
  "record.section-holds": { sentence: "A part (Section, Side, Page) holds Fields, child Lists, Readings, Progress, Statements or Acts." },
27274
- "record.section-reads": { sentence: "A Field a part shows as a value only (`edit: false`) is one that part draws.", stored: true },
27275
27267
  "record.compose": {
27276
27268
  sentence: "A Record's `compose` is stated instead of parts (it holds no children), on a page `door`, by an App with no Books whose records have no owned stages (`status.stages`) and no picture (`image`) \u2014 a composed page draws neither. Each name is a field of the entity, an Act of the app on the record or a child entity linked to it; one `Title`, and each field, Act and child drawn once \u2014 a field by `Field` or `Document`, never the title or the status, which `Title` and `Status` draw. The first `Act` the page itself draws is its next step and every other one secondary; an `Act` is a button alone, never one making a set of papers. A `Bar` holds no `Rows` and no `Document`, and an `Act` in it that destroys or corrects what is done (`danger`, or a move back) stands in the record's \u22EF instead (the Bar piece's own row says how it draws). A `Panel` draws nothing in the page: it is one entry of the record's \u22EF, named by its `title`, opening its pieces in a panel beside the page; it stands directly in the root `Column`, holds `Rows`, `Field`, `Act`, `Group` and `Document` \u2014 never `Title`, `Status`, `Bar`, `Copy` or another `Panel` \u2014 and an `Act` directly in it ends the panel's name, secondary. A `Group` is headed once, by its `title`: holding one `Rows`, that heading carries the rows' count and their add in place of the rows' own, and the `Act`s the group holds end its line. A `Copy` takes a text field's words as the page holds them now, a `Document`'s words not yet saved among them; a field a `Copy` alone names is read, never written."
27277
27269
  },
27278
- "record.sections": {
27270
+ "record.parts": {
27279
27271
  sentence: "A List laid out as a table, cards or a gantt opens a Record whose Sections hold the fields people write, a files Field per paper, a child List per child and the record's Acts.",
27280
27272
  decide: true,
27281
27273
  design: "sections"
@@ -27285,7 +27277,7 @@ var MODEL_RULES = {
27285
27277
  decide: true,
27286
27278
  design: "sections"
27287
27279
  },
27288
- "record.children": {
27280
+ "record.lists": {
27289
27281
  sentence: "A table some App records, whose rows other rows each belong to (a one-row link to it, never its status history), draws some of them under its row \u2014 a child List in its Record, a List with `layout` roster or lanes by that link, or an App over them working inside one of its rows at a time (`scope`).",
27290
27282
  decide: true,
27291
27283
  design: "children"
@@ -27340,94 +27332,78 @@ var MODEL_RULES = {
27340
27332
  },
27341
27333
  "record.field-heading": { sentence: "A Field drawn as a control states no `label` or `when`: only a gallery or prose Field (`block`) has a heading and a condition." },
27342
27334
  // ─── Blocks ──────────────────────────────────────────────────────────────────
27343
- "block.child-link": { sentence: "A child List reads a child linked to the record; `via` names the link where the child links more than once." },
27344
- "block.row-blocks": {
27335
+ "list.child-link": { sentence: "A child List reads a child linked to the record; `via` names the link where the child links more than once." },
27336
+ "list.row-blocks": {
27345
27337
  sentence: "The Record of a List below the register \u2014 a rows List's, a document's under its lines, a roster's cells' \u2014 holds its Fields and the Lists of its rows' own children, each a child List as one level up over an entity linking to that List's, checked against that List's entity; nothing else. A List below a row states no `under`, `pick`, `expect` or desk, and a List stating `expect`, `under` or a desk holds no List below its rows."
27346
27338
  },
27347
- "block.column-drawn": {
27348
- sentence: "A child List's `columns` never name what the child's row draws (title, subtitle, image, status, figure, or the bound its figure's meter reads against) nor its link back, and name a field once; an `expect` List's figure is its filled-in-place column."
27349
- },
27350
- "block.columns-budget": {
27351
- sentence: "A child List's `columns`, each at the width of its kind, fit the room a desk leaves beside its name and the row's own status, deadline and figure.",
27352
- authoring: true
27353
- },
27354
- "block.title-back": {
27339
+ "list.title-back": {
27355
27340
  sentence: "Where a child's title is its link back to the record a child List lists it under, its first other `row.subtitle` names it there \u2014 a text, a code, a select, a link or a member, never a number or a date."
27356
27341
  },
27357
- "block.where": {
27342
+ "list.where": {
27358
27343
  sentence: "A child List's `where` names single selects the child requires, by options they hold \u2014 one held at a single option is never also a column \u2014 and the child's yes/nos by value, numbers and dates by bounds, and fields by `{ \"empty\": true | false }`. A yes/no, a bound or a blank is never held: a child List adds rows only at a stored yes/no's start, adds none under a bound or a blank, and never picks, expects or stands lines under documents."
27359
27344
  },
27360
- "block.under": {
27345
+ "list.under": {
27361
27346
  sentence: "`under` names the child's one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines' columns never read the document (its heading does) nor hold long text."
27362
27347
  },
27363
- "block.under-once": { sentence: "Documents drawn over their lines (`under`) are never listed again as a List of their own." },
27364
- "block.edits": {
27348
+ "list.under-once": { sentence: "Documents drawn over their lines (`under`) are never listed again as a List of their own." },
27349
+ "list.edits": {
27365
27350
  sentence: "A child List's editable columns (`{ \"field\", \"edit\": true }`) name at most two of the child's columns or its figure that the App's saves of the child write \u2014 a number, a date, a few words, one option or a one-row link, never a files or long-text field, the List's `via` nor a link it must name alike (a link's `same`) \u2014 never on an `expect` List."
27366
27351
  },
27367
- "register.remove": {
27368
- sentence: "A List's Delete stands only where the App writes its rows: `remove: true` over rows it cannot write is refused. Absent, rows offer a Delete wherever the List writes them, and `remove: false` takes it away.",
27369
- stored: true
27370
- },
27371
- "register.remove-roster": {
27352
+ "list.remove-roster": {
27372
27353
  sentence: "A roster cells List's `remove: true` offers the Delete of the roster's rows, so it stands only beside a roster over rows the App writes."
27373
27354
  },
27374
- "register.attention": {
27355
+ "list.attention": {
27375
27356
  sentence: "`attention: false` keeps a table's or cards' rows in the List's own order \u2014 a first-come queue; a calendar, lanes, a gantt and a roster stand their rows in time and lift none."
27376
27357
  },
27377
- "block.remove": {
27378
- sentence: "A child List's `remove: true` stands only where the App writes those rows.",
27379
- stored: true
27380
- },
27381
- "block.remove-documents": {
27358
+ "list.remove-documents": {
27382
27359
  sentence: "The documents List inside a child List with `under` states `remove: true` only over documents the App writes."
27383
27360
  },
27384
- "block.compare": {
27361
+ "list.compare": {
27385
27362
  sentence: "A child List's `compare` names two different fields of the child, `before` and `after`: two files fields, or two values of one type (text but long text, a number, a date, a single select). Never with `expect`. Over files on a checklist layout, `before` is the check's `photo`. Over values: neither side is a column or something the row draws already (title, line, figure, status, deadline), two numbers share a notation, no column is long text, never on a checklist layout."
27386
27363
  },
27387
- "block.current": {
27364
+ "list.current": {
27388
27365
  sentence: "A child List's `current` names a date or number of the child (stored, or a formula yielding one) that orders a set of revisions: the row holding the latest value is current, the rest fold as superseded. Never with `group`, `under`, `expect`, `checklist`, `compare`, `post` or `pick`, nor beside a column of long text."
27389
27366
  },
27390
- "block.checklist": {
27367
+ "list.checklist": {
27391
27368
  sentence: "A child List with `layout: { checklist }` reads a task entity's rows as checks \u2014 never under documents, grouped, edited in place or picked together by an Act over several, its columns no files; `photo` names a files field of the child, `clause` a one-row link of it, `failed` options of its status, none `closed`; `passed` a yes/no of it, stored or a formula, setting each mark (yes passed, no failed, empty still to check) and standing without `task` or a status. With `expect` the lines of a standard are the checks, each marked and filled by the block's columns (a measured value, a note, a photo), so `photo` and `clause` are not named."
27392
27369
  },
27393
- "block.pick": {
27370
+ "list.pick": {
27394
27371
  sentence: "A child List's `pick` links rows of the child through `via` \u2014 an optional one-row link a person writes, no Act writes, no other rule compares rows by and no `under` List stands lines under \u2014 paired with a many-link of the record its own save writes; the child's `read_scope` reads a row naming no record through `via`; never with `expect` or `under`, nor where the App's saves leave out the child or the many-link."
27395
27372
  },
27396
- "block.narrow": {
27373
+ "list.narrow": {
27397
27374
  sentence: "A child List's `filters`, `search`, `group` and `sort` follow the register List's rules over the child's rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, nor on an `expect` List; an `under` List is grouped by its documents."
27398
27375
  },
27399
- "block.expect": {
27376
+ "list.expect": {
27400
27377
  sentence: "`expect` names the child's title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the List; its `columns` name a field filled in place \u2014 unless the List's layout is a desk, whose lines are ticked \u2014 and its `create` takes the file where the child is pictured by one."
27401
27378
  },
27402
- "block.expect-fills": {
27379
+ "list.expect-fills": {
27403
27380
  sentence: "An expected line is made from its key and its first filled column \u2014 a desk's from its key alone, by its tick \u2014 so every other field the child requires has a default, a start or a `default_from`, or is read first."
27404
27381
  },
27405
- "block.expect-of": {
27382
+ "list.expect-of": {
27406
27383
  sentence: "An `expect` object's `of` or `only` needs a select: `of` names the record's multi-select of the title's options; `only` names options of a single select, never with `of` or a `where` on it, one List per option; an Act's `paper` is drawn in one."
27407
27384
  },
27408
- "block.post": {
27385
+ "list.post": {
27409
27386
  sentence: `A child List with \`layout: { desk }\` needs \`expect\` on a one-row link \u2014 never a select, nor with \`under\`, \`pick\`, a checklist layout, \`compare\` or a \`create\` list, its line made by a tick from its key alone: its \`bodies\` are plain text fields of the record a person writes, drawn nowhere else on it; its \`text\` is a text of the child, stored or a formula yielding text \u2014 a formula reading only stored values of its row, lookups through the link back to the record or through \`expect\`, and formulas of the row over those, never the clock; its \`media\` a files field of the record and its \`link\` a text field of the record; its \`network\` a single select of the entity the link names, and \`networks\` holds options of that select, by alias, each to one of ${SOCIAL_NETWORKS.join(", ")}; its \`group\` is another single select of that entity.`
27410
27387
  },
27411
- "block.funnel": {
27388
+ "list.funnel": {
27412
27389
  sentence: `A Reading's \`steps\` (a funnel) are ${FUNNEL_STEPS.min} to ${FUNNEL_STEPS.max} numbers of its child, each named once \u2014 stored, a formula or a rollup \u2014 all in one notation, never a percent nor a unit read on each row.`
27413
27390
  },
27414
- "block.list": {
27391
+ "list.list": {
27415
27392
  sentence: "A read-only child List (`same`, `app`, or a Record that is `edit: false`) lists the rows of an entity that link to the record by its one link to it (`via` where it has several); with `same`, a one-row link of the record, it lists the rows sharing that link's row with the record \u2014 the record left out \u2014 by the listed entity's one link to that row's entity (`via` where it has several). `same` stands in a Record, never on a dashboard. Its Record's Fields \u2014 at least one, each a field of the listed entity named once, never its title nor one of the `columns` \u2014 are what a row shows, read-only, when it is opened in place."
27416
27393
  },
27417
- "block.list-opens": {
27394
+ "list.list-opens": {
27418
27395
  sentence: "A read-only child List's row opens in the record's drawer as a record: its entity is one a List of the App draws \u2014 the register's, a Book's, or a log's \u2014 or the List's `app` names the register app that works it.",
27419
27396
  authoring: true
27420
27397
  },
27421
- "block.figures": {
27398
+ "list.figures": {
27422
27399
  sentence: `A Record's own figures (Readings with no \`entity\` and no \`by\`, whose \`value\` names the field) are ${FIGURE_TILES.min} to ${FIGURE_TILES.max} numbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number the App's saves leave out \u2014 each placed once on the record.`
27423
27400
  },
27424
- "block.agenda": { sentence: "An agenda List's child names a date on its row line (`records` title or subtitle); the layout's `start` is a date of the record." },
27425
- "block.timeline": { sentence: "A timeline List's child holds a date to place its entries by." },
27426
- "block.create-boolean": { sentence: "A document's and a roster's cells' `create` is `true` or `false`: their add asks the table's defaults." },
27427
- "block.via-path": { sentence: "A `<link>.<link>` `via` reaches rows through a row of their own and only reads them: a List writing its rows lists a child linked to the row it stands under." },
27428
- "block.read-label": { sentence: "A List that only reads its rows \u2014 a read-only child List, an Outside's List, a dashboard's List \u2014 states its `label`." },
27429
- "block.create-empty": { sentence: "A child List that adds rows asks the fields of its add: where the table's defaults ask none, state `create: false`, or the fields it asks." },
27430
- "block.field-kind": { sentence: "A gallery Field names a files field of the record, and a prose Field a text field.", stored: true },
27401
+ "list.agenda": { sentence: "An agenda List's child names a date on its row line (`records` title or subtitle); the layout's `start` is a date of the record." },
27402
+ "list.timeline": { sentence: "A timeline List's child holds a date to place its entries by." },
27403
+ "list.create-boolean": { sentence: "A document's and a roster's cells' `create` is `true` or `false`: their add asks the table's defaults." },
27404
+ "list.via-path": { sentence: "A `<link>.<link>` `via` reaches rows through a row of their own and only reads them: a List writing its rows lists a child linked to the row it stands under." },
27405
+ "list.read-label": { sentence: "A List that only reads its rows \u2014 a read-only child List, an Outside's List, a dashboard's List \u2014 states its `label`." },
27406
+ "list.create-empty": { sentence: "A child List that adds rows asks the fields of its add: where the table's defaults ask none, state `create: false`, or the fields it asks." },
27431
27407
  "statement.lines": {
27432
27408
  sentence: "A Statement's `lines` are numbers of the record, each once \u2014 the first stating no `op`, every other one, the last an `equals` \u2014 each `equals` a formula, rollup or lookup, and a `rate` a number of the record."
27433
27409
  },
@@ -27437,33 +27413,33 @@ var MODEL_RULES = {
27437
27413
  "statement.parts-keyed": {
27438
27414
  sentence: "A line's `parts` add up to its figure, so no person keys it: a formula, rollup or lookup, a value the server copies when the record is made (`default_from`) that no `create` asks and no save or Act writes, or a stored number the App's saves leave out \u2014 the line draws it read; each part's `figure` is frozen when its row was certified \u2014 a stored number an Act fills, the server copies or the App's saves leave out, never one a person keys or the server recomputes \u2014 and no person keys the moments `at` and `before` either."
27439
27415
  },
27440
- "block.read-scope": {
27416
+ "list.read-scope": {
27441
27417
  sentence: "A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history's, which takes its record's."
27442
27418
  },
27443
27419
  "record.section-owners": {
27444
27420
  sentence: "A Section holding acts offered at stages that different owners work \u2014 a role apiece, or a member field apiece \u2014 is two jobs in one: each owner's part is a Section of its own.",
27445
27421
  note: true
27446
27422
  },
27447
- "register.unworked-stage": {
27423
+ "list.unworked-stage": {
27448
27424
  sentence: "A register lists rows (every open one, else those its `where` keeps) at a stage that no Act of the App is offered at and no Section stands at, though the App states the stages it works: the rows only sit in its list, so keep them out with `where` or leave them to the App that works them.",
27449
27425
  note: true
27450
27426
  },
27451
- "block.agenda-note": {
27427
+ "list.agenda-note": {
27452
27428
  sentence: "Rows each on a day and within it (at an hour, in a part of the day) listed as a table: a List with `layout: { agenda }` draws them by day.",
27453
27429
  note: true,
27454
27430
  design: "agenda"
27455
27431
  },
27456
- "block.under-note": {
27432
+ "list.under-note": {
27457
27433
  sentence: "Rows holding a paper that cover another List's lines, listed apart with no add of their own stated: `under` draws them over the lines they cover.",
27458
27434
  note: true,
27459
27435
  design: "under"
27460
27436
  },
27461
- "block.timeline-note": {
27437
+ "list.timeline-note": {
27462
27438
  sentence: 'Rows each what happened on a day and who \u2014 a date on their line, a member, no status and no figure \u2014 listed as a table: a List with `layout: "timeline"` draws them as a log, newest first.',
27463
27439
  note: true,
27464
27440
  design: "timeline"
27465
27441
  },
27466
- "block.expect-note": {
27442
+ "list.expect-note": {
27467
27443
  sentence: "Rows each a kind of paper, listed with no `expect` and no `where` on the kind, that no Act files a call into or `fills`: the lines still missing read as what a record needs.",
27468
27444
  note: true,
27469
27445
  design: "expect"
@@ -27475,22 +27451,12 @@ var MODEL_RULES = {
27475
27451
  decide: true,
27476
27452
  design: "moves"
27477
27453
  },
27478
- "app.adds-entity": { sentence: "A stored body's `adds` name an entity the app reaches.", stored: true },
27479
27454
  "app.adds-field": { sentence: "A picker's `add` names fields of its table a person writes, each once." },
27480
- "app.picks-only-entity": { sentence: "A Field or a row title that states `add: false` stands in an entity the app reaches.", stored: true },
27481
27455
  "app.picks-only-field": { sentence: "`add: false` stands at a link, once, to an entity whose add the app states." },
27482
- "app.cards-entity": { sentence: "A Field or a row title that states `record` or `remove` stands at a link to an entity the app reaches.", stored: true },
27483
- "app.remove": {
27484
- sentence: "An App that writes its register's records offers a Delete of them wherever the List writes them, until the List says `remove: false`.",
27485
- decide: true,
27486
- design: "sections",
27487
- stored: true
27488
- },
27489
27456
  "app.edits": {
27490
- sentence: "A stored body states the fields its saves may change (`edits`, by entity): the fields its surfaces write less what its own Acts change \u2014 a record's save, a child row's, an in-place edit, a lane, gantt or plan move; a body stating none saves nothing.",
27457
+ sentence: "The fields an app's saves may change are the fields its surfaces write \u2014 a record's save, a child row's, an in-place column, a lane, gantt or plan move \u2014 less what its own Acts change; the designer notes an app whose saves change none.",
27491
27458
  decide: true,
27492
- design: "sections",
27493
- stored: true
27459
+ design: "sections"
27494
27460
  },
27495
27461
  "act.set-not-asked": {
27496
27462
  sentence: 'A field an Act `set`s (an option, a value, `{ "now": true }`, `{ "me": true }`, an input) or `sign`s is the Act\'s outcome: the App\'s `create` never asks it, and a required one takes a `default`. A `formula` only starts a value, which a person may change after.'
@@ -27587,18 +27553,18 @@ function oneLineEach(findings) {
27587
27553
  function ruleLines(findings) {
27588
27554
  const named = [...new Set(findings.flatMap((finding) => finding.rule === void 0 ? [] : [finding.rule]))];
27589
27555
  if (named.length === 0) return [];
27590
- return ["", " The rules named above (lotics docs model/<rule> prints one):", ...named.map((rule) => ` ${rule} \u2014 ${MODEL_RULES[rule].sentence}`)];
27556
+ return ["", " The rules named above (lotics docs reference/<rule> prints one):", ...named.map((rule) => ` ${rule} \u2014 ${APP_RULES[rule].sentence}`)];
27591
27557
  }
27592
- function isModelRule(rule) {
27593
- return rule !== void 0 && Object.hasOwn(MODEL_RULES, rule);
27558
+ function isAppRule(rule) {
27559
+ return rule !== void 0 && Object.hasOwn(APP_RULES, rule);
27594
27560
  }
27595
27561
  function reportServerFindings(findings, heading) {
27596
27562
  if (findings.length === 0) return;
27597
27563
  const reported = findings.map((finding) => ({
27598
27564
  path: finding.at,
27599
- message: isModelRule(finding.rule) ? finding.message : `${finding.message}${finding.rule === void 0 ? "" : ` [${finding.rule}]`}`,
27565
+ message: isAppRule(finding.rule) ? finding.message : `${finding.message}${finding.rule === void 0 ? "" : ` [${finding.rule}]`}`,
27600
27566
  severity: finding.note === true ? "note" : "error",
27601
- ...isModelRule(finding.rule) ? { rule: finding.rule } : {}
27567
+ ...isAppRule(finding.rule) ? { rule: finding.rule } : {}
27602
27568
  }));
27603
27569
  const lines = oneLineEach(reported);
27604
27570
  console.error(`