@lotics/cli 0.316.16 → 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 +2 -2
- package/docs/document_templates.md +3 -1
- package/package.json +1 -1
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.
|
|
1733
|
+
var VERSION = "0.316.17";
|
|
1734
1734
|
var APP_SDK_VERSION = "0.124.7";
|
|
1735
1735
|
|
|
1736
1736
|
// src/timezone.ts
|
|
@@ -1798,7 +1798,7 @@ var workflow_steps_default = '<!-- Generated by backend/scripts/generate_workflo
|
|
|
1798
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"}}` (`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';
|
|
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';
|
|
@@ -79,7 +79,9 @@ this host, give the person the file and a `person` drop link.
|
|
|
79
79
|
`{{FOR … IN name}}` array, `{{IF name}}` boolean), and a marker you did not declare is still added
|
|
80
80
|
(`derived_variables`). At generate time `data` needs a key for **every** marker, `""` for a blank one. A marker may run
|
|
81
81
|
across formatting runs and is still read whole. In a table, the row holding `{{FOR line IN lines}}` and the row holding
|
|
82
|
-
`{{END-FOR line}}` enclose the row that repeats.
|
|
82
|
+
`{{END-FOR line}}` enclose the row that repeats. A newline in a value prints as a line break and a tab as a tab;
|
|
83
|
+
a tab the paper prints only when a blank is empty is written `{{IF !name}}{{"\t"}}……{{END-IF}}`, since the
|
|
84
|
+
form's own tab stays put inside an `{{IF}}` on the same line.
|
|
83
85
|
- **`email`** — inline HTML + Handlebars, with `template` and `variables`; optionally a default `subject` (it takes
|
|
84
86
|
`{{variable}}` too) and `default_to`, `default_cc`, `default_bcc`. Body and subject render from the data at send time.
|
|
85
87
|
|