@lotics/cli 0.305.0 → 0.305.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/dist/src/cli.js +9 -36
- package/dist/src/client.d.ts +1 -1
- package/docs/data_model.md +1 -2
- package/docs/workflows.md +14 -55
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -26259,7 +26259,7 @@ var tableDateFieldSchema = tableFieldBaseSchema.extend({
|
|
|
26259
26259
|
format: dateFormatSchema.optional(),
|
|
26260
26260
|
timezone: zod_default.string().optional().describe("IANA timezone"),
|
|
26261
26261
|
derive_from: zod_default.enum(["created_at", "updated_at"]).optional().describe(
|
|
26262
|
-
"Auto-populate from the row's system timestamp. `created_at` stamps once at insert time; `updated_at` re-stamps on every record edit. A derived field is read-only: a write naming it is refused, as for formula/rollup/lookup.
|
|
26262
|
+
"Auto-populate from the row's system timestamp. `created_at` stamps once at insert time; `updated_at` re-stamps on every record edit. A derived field is read-only: a write naming it is refused, as for formula/rollup/lookup."
|
|
26263
26263
|
),
|
|
26264
26264
|
// A derived date is read-only, so derive_from and default_value are mutually
|
|
26265
26265
|
// exclusive — enforced in validateFieldDefaultValue.
|
|
@@ -26675,30 +26675,6 @@ var fieldConfigEntrySchema = zod_default.object({
|
|
|
26675
26675
|
|
|
26676
26676
|
// ../shared/src/schemas/table_records.ts
|
|
26677
26677
|
var tableWorkflowEventSchema = zod_default.enum(["after_create", "after_update", "after_delete"]);
|
|
26678
|
-
var tableWorkflowSchema = zod_default.object({
|
|
26679
|
-
id: zod_default.string().describe("Unique table-workflow binding ID (tbw_ prefix)"),
|
|
26680
|
-
name: zod_default.string().describe("Human-readable name"),
|
|
26681
|
-
description: zod_default.string().describe("Description of what this workflow does"),
|
|
26682
|
-
enabled: zod_default.boolean().describe("Whether this workflow is active"),
|
|
26683
|
-
event: tableWorkflowEventSchema.describe(
|
|
26684
|
-
"Lifecycle event that fires this workflow, after the write commits."
|
|
26685
|
-
),
|
|
26686
|
-
workflow_id: zod_default.string().describe(
|
|
26687
|
-
"ID of the workflow row that owns this binding's steps. The workflow's steps_v2 is the source of truth for the AST."
|
|
26688
|
-
),
|
|
26689
|
-
// Recursive v2 expression AST. Lazy schema would crash the OpenAPI walker;
|
|
26690
|
-
// runtime is `unknown` and the casted Zod type preserves the compile-time
|
|
26691
|
-
// signature for callers. Write paths re-parse with the strict schema before
|
|
26692
|
-
// storage.
|
|
26693
|
-
if: zod_default.unknown().nullable().optional().describe(
|
|
26694
|
-
"Optional pre-execution gate. Evaluated against the trigger context (record / prev_record / changes / runtime); the workflow fires only when truthy. Skip gating means the workflow always fires."
|
|
26695
|
-
),
|
|
26696
|
-
created_at: zod_default.string().describe("When the binding was created"),
|
|
26697
|
-
updated_at: zod_default.string().describe("When the binding was last modified"),
|
|
26698
|
-
archived_at: zod_default.string().nullable().optional().describe(
|
|
26699
|
-
"Set when the binding was removed (soft-delete). An archived binding never fires and is hidden from active listings, but is restorable; null/absent = active."
|
|
26700
|
-
)
|
|
26701
|
-
});
|
|
26702
26678
|
var tableUniqueSchema = zod_default.array(zod_default.array(zod_default.string()).min(1)).describe(
|
|
26703
26679
|
"Field-key tuples whose values no two live rows share: a create or update that would repeat every value of a tuple is refused. Members are text, number, date, single-select or cardinality-one link fields; a row with any member empty never collides."
|
|
26704
26680
|
);
|
|
@@ -26709,9 +26685,6 @@ var tableSchema = zod_default.object({
|
|
|
26709
26685
|
description: zod_default.string().describe("Description of the table's purpose"),
|
|
26710
26686
|
position: zod_default.number().int().optional().nullable().describe("Display ordering index for the table. Lower numbers are shown first. "),
|
|
26711
26687
|
fields: tableFieldsSchema.describe("Array of field definitions for this table"),
|
|
26712
|
-
workflows: zod_default.array(tableWorkflowSchema).optional().describe(
|
|
26713
|
-
"Workflows attached to this table that fire after its record writes commit."
|
|
26714
|
-
),
|
|
26715
26688
|
private_filters: tableRecordFiltersGroupNodeSchema.nullable().optional().describe("Row-level private filters AND'd to every query as a security boundary (admin bypass)"),
|
|
26716
26689
|
colors: tableRecordColorsSchema.nullable().optional().describe("Color rules for styling records based on filter conditions"),
|
|
26717
26690
|
approvers: zod_default.array(principalSchema).nullable().optional().describe(
|
|
@@ -44512,8 +44485,8 @@ async function saveSignedLinks(links, dir) {
|
|
|
44512
44485
|
}
|
|
44513
44486
|
|
|
44514
44487
|
// src/version.ts
|
|
44515
|
-
var VERSION = "0.305.
|
|
44516
|
-
var APP_SDK_VERSION = "0.120.
|
|
44488
|
+
var VERSION = "0.305.1";
|
|
44489
|
+
var APP_SDK_VERSION = "0.120.2";
|
|
44517
44490
|
|
|
44518
44491
|
// src/timezone.ts
|
|
44519
44492
|
function machineTimezone() {
|
|
@@ -44538,7 +44511,7 @@ var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOn
|
|
|
44538
44511
|
var examples_default = '# Worked examples\n\nEach section is one treatment, cut from a complete model of an invented business: the job, the keys to\nread, where the treatment is wrong, and the excerpt \u2014 an app the check passes with no finding, as stated;\nthe `records` of every entity it draws; those entities\' fields it names, by what each holds and how it is\ndrawn; and the templates it makes. Left out: labels, descriptions, `required`, `unique`, `default`, a link\'s\npairing, how a computed field computes, and template contents. Choosing among them: the `design` reference.\nEvery key: the `model` reference, at the pages each section names under **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.** `register.layout: "calendar"`; 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 "entities": [\n {\n "alias": "service_visit",\n "fields": [\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "scheduled_for", "type": "date", "format": "datetime"},\n {"alias": "ends_at", "type": "date", "format": "datetime"},\n {"alias": "engineer", "type": "select_record_link", "target_entity": "engineer", "cardinality": "one"},\n {\n "alias": "job",\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n {"alias": "booked_on", "type": "date"},\n {"alias": "parts_ready_on", "type": "date"},\n {"alias": "arrived_on", "type": "date"},\n {"alias": "hours", "type": "number", "notation": {"style": "unit", "unit": "h"}},\n {"alias": "report", "type": "text", "format": "markdown"},\n {"alias": "completed_on", "type": "date"}\n ]\n }\n ],\n "records": {\n "service_visit": {\n "title": "customer",\n "subtitle": ["scheduled_for", "ends_at"],\n "status": {\n "milestones": ["booked_on", {"field": "parts_ready_on", "when": {"job": ["repair"]}}, "arrived_on", "completed_on"],\n "closed": ["completed_on"]\n },\n "figure": "hours",\n "due": ["scheduled_for"]\n }\n },\n "apps": [\n {\n "alias": "visits",\n "name": "Service visits",\n "description": "Engineers\' visits to customers on site, booked and completed by the day.",\n "icon": "calendar",\n "entity": "service_visit",\n "register": {\n "columns": ["engineer", "job"],\n "filters": ["customer"],\n "sort": {"field": "scheduled_for"},\n "layout": "calendar",\n "create": ["customer", "job", "scheduled_for", "ends_at", "engineer"],\n "readings": [\n {"metric": "service_visit", "where": {"job": ["repair"]}, "label": "Repairs"},\n {"breakdown": "service_visit", "by": "engineer", "value": "hours"}\n ]\n },\n "record": {\n "sections": [\n {"title": "Visit", "at": ["booked_on", "parts_ready_on"], "fields": ["job"], "acts": ["reschedule"]},\n {"title": "Report", "at": ["arrived_on"], "fields": ["engineer", "report"], "acts": ["complete"]}\n ]\n },\n "acts": [\n {\n "alias": "complete",\n "label": "Complete visit",\n "requires": ["engineer"],\n "asks": ["hours", {"input": "summary", "label": "What was done", "type": "long_text", "required": true}],\n "set": {"report": "input:summary", "completed_on": "now"}\n },\n {"alias": "reschedule", "label": "Reschedule", "asks": ["scheduled_for", "ends_at"]}\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.** `register.layout: "lanes"` with `lanes` 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 "entities": [\n {\n "alias": "service_visit",\n "fields": [\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "scheduled_for", "type": "date", "format": "datetime"},\n {"alias": "ends_at", "type": "date", "format": "datetime"},\n {"alias": "engineer", "type": "select_record_link", "target_entity": "engineer", "cardinality": "one"},\n {\n "alias": "job",\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n {"alias": "booked_on", "type": "date"},\n {"alias": "parts_ready_on", "type": "date"},\n {"alias": "arrived_on", "type": "date"},\n {"alias": "hours", "type": "number", "notation": {"style": "unit", "unit": "h"}},\n {"alias": "completed_on", "type": "date"}\n ]\n },\n {\n "alias": "engineer",\n "fields": [\n {"alias": "name", "type": "text"},\n {"alias": "base", "type": "select_record_link", "target_entity": "warehouse", "cardinality": "one"}\n ]\n }\n ],\n "records": {\n "service_visit": {\n "title": "customer",\n "subtitle": ["scheduled_for", "ends_at"],\n "status": {\n "milestones": ["booked_on", {"field": "parts_ready_on", "when": {"job": ["repair"]}}, "arrived_on", "completed_on"],\n "closed": ["completed_on"]\n },\n "figure": "hours",\n "due": ["scheduled_for"]\n },\n "engineer": {"title": "name", "subtitle": ["base"], "party": "person"}\n },\n "apps": [\n {\n "alias": "dispatch",\n "name": "Dispatch",\n "description": "Each engineer\'s visits across the day, and the free time between them to book into.",\n "icon": "wrench",\n "entity": "service_visit",\n "register": {\n "columns": ["job"],\n "search": ["customer"],\n "layout": "lanes",\n "lanes": "engineer",\n "create": ["customer", "job"]\n },\n "record": {"door": "drawer", "sections": [{"title": "Booking", "fields": ["engineer", "job"]}]}\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.** `register.layout: "gantt"` and `register.gantt.start`; with no end stated, the bar ends at the entity\'s first `due`.\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 "entities": [\n {\n "alias": "phieu_thue",\n "fields": [\n {\n "alias": "khach_hang",\n "type": "select_record_link",\n "target_entity": "khach_hang",\n "cardinality": "one"\n },\n {"alias": "so_phieu", "type": "text"},\n {\n "alias": "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 {"alias": "phu_trach", "type": "select_member"},\n {"alias": "ngay_giao", "type": "date"},\n {"alias": "han_tra", "type": "date"},\n {"alias": "ngay_tra", "type": "date"},\n {\n "alias": "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 {"alias": "so_ngay", "type": "formula"},\n {"alias": "tong_tien", "type": "rollup"}\n ]\n }\n ],\n "records": {\n "phieu_thue": {\n "title": "khach_hang",\n "subtitle": ["so_phieu"],\n "status": {"field": "trang_thai", "closed": ["da_tra", "huy"], "history": "lich_su_phieu"},\n "figure": "tong_tien",\n "due": ["han_tra"]\n }\n },\n "apps": [\n {\n "alias": "tien_do_thue",\n "name": "Ti\u1EBFn \u0111\u1ED9 cho thu\xEA",\n "description": "M\u1ED7i phi\u1EBFu thu\xEA m\u1ED9t thanh t\u1EEB ng\xE0y giao m\xE1y \u0111\u1EBFn h\u1EA1n tr\u1EA3, \u0111\u1ECDc theo h\xF4m nay: phi\u1EBFu n\xE0o s\u1EAFp \u0111\u1EBFn h\u1EA1n, phi\u1EBFu n\xE0o qu\xE1 h\u1EA1n; k\xE9o thanh \u0111\u1EC3 d\u1EDDi ng\xE0y giao ho\u1EB7c gia h\u1EA1n tr\u1EA3, v\xE0 nh\u1EADn m\xE1y v\u1EC1 khi kh\xE1ch tr\u1EA3.",\n "icon": "calendar-range",\n "theme": {"color": "teal"},\n "entity": "phieu_thue",\n "register": {\n "columns": ["phu_trach"],\n "filters": ["phu_trach", "khach_hang"],\n "layout": "gantt",\n "gantt": {"start": "ngay_giao"},\n "remove": false,\n "create": false,\n "readings": [\n {"metric": "phieu_thue", "where": {"trang_thai": ["dang_thue"]}, "label": "Phi\u1EBFu \u0111ang cho thu\xEA"},\n {\n "metric": "phieu_thue",\n "value": "tong_tien",\n "where": {"trang_thai": ["dang_thue"]},\n "label": "Ti\u1EC1n thu\xEA \u0111ang ch\u1EA1y"\n }\n ]\n },\n "record": {\n "sections": [\n {"title": "Phi\u1EBFu thu\xEA", "fields": ["ngay_giao", "han_tra"]},\n {"title": "\u0110ang thu\xEA", "at": ["dang_thue"], "fields": ["so_ngay", "ngay_tra"], "acts": ["nhan_tra"]}\n ]\n },\n "acts": [\n {\n "alias": "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", "ngay_tra": "now"},\n "confirm": true\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.** `register.layout: "roster"` over the people; `register.roster` names the child whose rows 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 "entities": [\n {\n "alias": "nha_si",\n "fields": [\n {"alias": "ho_ten", "type": "text"},\n {\n "alias": "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 {"alias": "dien_thoai", "type": "text"}\n ]\n },\n {\n "alias": "lich_truc",\n "fields": [\n {"alias": "nha_si", "type": "select_record_link", "target_entity": "nha_si", "cardinality": "one"},\n {\n "alias": "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 {"alias": "ngay", "type": "date"},\n {"alias": "ghe", "type": "select_record_link", "target_entity": "ghe", "cardinality": "one"}\n ]\n }\n ],\n "records": {\n "nha_si": {"title": "ho_ten", "subtitle": ["chuyen_mon"], "party": "person"},\n "lich_truc": {"title": "ca", "subtitle": ["ngay", "ghe"]}\n },\n "apps": [\n {\n "alias": "lich_truc",\n "name": "L\u1ECBch tr\u1EF1c",\n "description": "Ai tr\u1EF1c ca n\xE0o, \u1EDF gh\u1EBF n\xE0o, trong tu\u1EA7n v\xE0 trong th\xE1ng.",\n "icon": "users",\n "entity": "nha_si",\n "register": {\n "filters": ["chuyen_mon"],\n "layout": "roster",\n "roster": "lich_truc",\n "create": ["ho_ten", "chuyen_mon", "dien_thoai"]\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.** `register.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/apps`\n\n**Not when.** Rows with no picture \u2014 cards of words are a table drawn worse.\n\n```json\n{\n "entities": [\n {\n "alias": "stock_item",\n "fields": [\n {"alias": "name", "type": "text"},\n {\n "alias": "warehouse",\n "type": "select_record_link",\n "target_entity": "warehouse",\n "cardinality": "one"\n },\n {"alias": "photo", "type": "files"},\n {"alias": "sku", "type": "text"},\n {\n "alias": "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 {"alias": "on_hand", "type": "number"},\n {"alias": "minimum", "type": "number"},\n {"alias": "price", "type": "number", "notation": {"style": "currency", "currency": "USD"}},\n {"alias": "supplier_page", "type": "text", "format": "link"},\n {"alias": "last_counted", "type": "date"},\n {"alias": "counted_by", "type": "select_member"},\n {"alias": "count_note", "type": "text"}\n ]\n },\n {\n "alias": "order_line",\n "fields": [\n {"alias": "item", "type": "select_record_link", "target_entity": "stock_item", "cardinality": "one"},\n {"alias": "order", "type": "select_record_link", "target_entity": "order", "cardinality": "one"},\n {"alias": "quantity", "type": "number"},\n {"alias": "shipped", "type": "number"},\n {"alias": "picked", "type": "number"},\n {"alias": "unit_price", "type": "number", "notation": {"style": "currency", "currency": "USD"}},\n {"alias": "amount", "type": "formula"},\n {"alias": "category", "type": "lookup"}\n ]\n }\n ],\n "records": {\n "stock_item": {\n "title": "name",\n "subtitle": ["sku", "category"],\n "image": "photo",\n "figure": "on_hand",\n "limits": {"on_hand": 400},\n "gates": {"on_hand": "minimum"}\n },\n "order_line": {\n "title": "item",\n "subtitle": ["order", "unit_price"],\n "figure": "amount",\n "limits": {"shipped": "quantity", "picked": "quantity"}\n }\n },\n "apps": [\n {\n "alias": "stock",\n "name": "Stock",\n "description": "What the store holds of each item, against its shelf and its minimum.",\n "icon": "boxes",\n "theme": {"color": "teal"},\n "entity": "stock_item",\n "scope": "warehouse",\n "register": {\n "columns": ["price"],\n "filters": ["on_hand"],\n "search": ["name", "sku"],\n "layout": "cards",\n "create": false,\n "export": true,\n "readings": [{"breakdown": "stock_item", "by": "category", "value": "on_hand"}]\n },\n "record": {\n "sections": [\n {"title": "Item", "fields": ["price", "minimum", "supplier_page"]},\n {"title": "Last count", "fields": ["last_counted", "counted_by", "count_note"], "acts": ["count"]},\n {"title": "On order", "blocks": [{"rows": "order_line", "columns": ["quantity"]}]}\n ]\n },\n "acts": [\n {\n "alias": "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",\n "counted_by": "me",\n "count_note": "input:remark"\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 `timeline` of touches, a `trend` of its orders, and its papers `expect`ed by kind.\n\n**Keys.** `model/records`, `model/timeline-and-agenda`, `model/blocks`, `model/reading-blocks`\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 "entities": [\n {\n "alias": "customer",\n "fields": [\n {"alias": "name", "type": "text"},\n {"alias": "logo", "type": "files"},\n {"alias": "code", "type": "text"},\n {\n "alias": "tier",\n "type": "select",\n "options": [\n {"alias": "key", "color": "violet"},\n {"alias": "standard", "color": "sky"},\n {"alias": "lapsed", "color": "gray"}\n ]\n },\n {"alias": "phone", "type": "text"},\n {"alias": "website", "type": "text", "format": "link"},\n {"alias": "account_manager", "type": "select_member"},\n {"alias": "credit_limit", "type": "number", "notation": {"style": "currency", "currency": "USD"}},\n {"alias": "outstanding", "type": "rollup"},\n {"alias": "over_limit", "type": "formula"},\n {\n "alias": "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 {"alias": "notes", "type": "text", "format": "markdown"}\n ]\n },\n {\n "alias": "order",\n "fields": [\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "reference", "type": "text"},\n {\n "alias": "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": "delivered", "color": "green"},\n {"alias": "cancelled", "color": "rose"}\n ]\n },\n {"alias": "placed", "type": "date"},\n {"alias": "due_date", "type": "date"},\n {"alias": "total", "type": "rollup"},\n {"alias": "balance", "type": "formula"},\n {"alias": "units_ordered", "type": "rollup"},\n {"alias": "units_shipped", "type": "rollup"},\n {"alias": "units_picked", "type": "rollup"},\n {"alias": "notes", "type": "text", "format": "markdown"},\n {"alias": "paid_in_full", "type": "formula"}\n ]\n },\n {\n "alias": "customer_paper",\n "fields": [\n {\n "alias": "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 {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "received_on", "type": "date"},\n {"alias": "file", "type": "files"}\n ]\n },\n {\n "alias": "touch",\n "fields": [\n {"alias": "said", "type": "text"},\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "by", "type": "select_member"}\n ]\n }\n ],\n "records": {\n "customer": {\n "title": "name",\n "subtitle": ["tier"],\n "image": "logo",\n "party": "organization",\n "figure": "outstanding",\n "limits": {"outstanding": "credit_limit"}\n },\n "order": {\n "title": "reference",\n "subtitle": ["customer", "placed"],\n "status": {"field": "stage", "closed": ["delivered", "cancelled"], "history": "order_stage"},\n "figure": "total",\n "limits": {"units_shipped": "units_ordered", "units_picked": "units_ordered"},\n "due": ["due_date"]\n },\n "customer_paper": {"title": "kind", "image": "file"},\n "touch": {"title": "said"}\n },\n "apps": [\n {\n "alias": "customers",\n "name": "Customers",\n "description": "Who we sell to, what they owe, and what was said.",\n "icon": "users",\n "theme": {"color": "violet"},\n "entity": "customer",\n "reads": "shared",\n "register": {\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 "readings": [\n {"breakdown": "customer", "by": "tier", "value": "outstanding"},\n {"breakdown": "customer", "by": "account_manager", "value": "outstanding"}\n ]\n },\n "record": {\n "door": "drawer",\n "sections": [\n {"title": "Contact", "fields": ["phone", "website"]},\n {\n "title": "Account",\n "fields": ["code", "account_manager", "credit_limit"],\n "acts": ["reassign", "lapse", "reinstate"]\n },\n {\n "title": "Orders",\n "blocks": [\n {\n "trend": "order",\n "over": "placed",\n "value": "total",\n "where": {"stage": ["confirmed", "picking", "shipped", "delivered"]},\n "label": "Order value"\n },\n {\n "rows": "order",\n "via": "customer",\n "title": "Open orders",\n "columns": ["due_date"],\n "create": false,\n "where": {"stage": ["draft", "confirmed", "picking", "shipped"]}\n },\n {\n "rows": "order",\n "via": "customer",\n "title": "Shipped, still owed",\n "columns": ["balance"],\n "create": false,\n "where": {"stage": ["shipped", "delivered"], "paid_in_full": false}\n }\n ]\n },\n {\n "title": "Account papers",\n "blocks": [\n {\n "rows": "customer_paper",\n "via": "customer",\n "columns": ["received_on"],\n "expect": "kind",\n "of": "papers_needed"\n }\n ]\n },\n {"title": "Contact log", "blocks": [{"timeline": "touch", "via": "customer"}]},\n {"title": "Notes", "blocks": [{"text": "notes"}]}\n ]\n },\n "acts": [\n {\n "alias": "lapse",\n "label": "Mark lapsed",\n "when": {"tier": ["key", "standard"]},\n "set": {"tier": "lapsed"},\n "confirm": true\n },\n {\n "alias": "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 "alias": "reassign",\n "label": "Reassign",\n "asks": [{"input": "to", "label": "Account manager", "type": "member", "required": true}],\n "set": {"account_manager": "input:to"}\n }\n ],\n "checks": [{"field": "over_limit"}]\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.** `expect` blocks 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/blocks`, `model/papers-and-recordings`\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 "entities": [\n {\n "alias": "ho_so",\n "fields": [\n {\n "alias": "khach_hang",\n "type": "select_record_link",\n "target_entity": "khach_hang",\n "cardinality": "one"\n },\n {"alias": "du_an", "type": "select_record_link", "target_entity": "du_an", "cardinality": "one"},\n {"alias": "ma_ho_so", "type": "autonumber"},\n {\n "alias": "dich_vu",\n "type": "select",\n "options": [{"alias": "tron_goi", "color": "blue"}, {"alias": "lam_ho_so", "color": "violet"}]\n },\n {\n "alias": "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 {\n "alias": "tinh_trang_nha_o",\n "type": "select",\n "options": [{"alias": "chua_co_nha", "color": "gray"}, {"alias": "nha_chat", "color": "orange"}]\n },\n {"alias": "giay_chung_nhan_so", "type": "text"},\n {"alias": "dien_tich_nha", "type": "number"},\n {"alias": "sale", "type": "select_member"},\n {"alias": "xu_ly", "type": "select_member"},\n {"alias": "can_bo_sung", "type": "boolean"},\n {"alias": "phi_dich_vu", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {\n "alias": "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 {"alias": "can_boc", "type": "text"},\n {"alias": "hoa_hong_sale", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {"alias": "ngay_thu_phi", "type": "date"},\n {"alias": "ngay_nhan_giay_to", "type": "date"},\n {"alias": "ngay_lap_ho_so", "type": "date"},\n {"alias": "ngay_nop_cdt", "type": "date"},\n {"alias": "ngay_cdt_duyet", "type": "date"},\n {"alias": "ngay_so_xd_duyet", "type": "date"},\n {"alias": "ngay_boc_tham", "type": "date"},\n {"alias": "ngay_ky_hdmb", "type": "date"},\n {"alias": "ngay_ban_giao", "type": "date"},\n {"alias": "bo_ho_so", "type": "files"},\n {"alias": "hop_dong_mua_ban", "type": "files"},\n {"alias": "giay_to", "type": "select_record_link", "target_entity": "giay_to", "cardinality": "many"},\n {\n "alias": "thanh_vien",\n "type": "select_record_link",\n "target_entity": "thanh_vien",\n "cardinality": "many"\n },\n {\n "alias": "phieu_thu",\n "type": "select_record_link",\n "target_entity": "phieu_thu",\n "cardinality": "many"\n },\n {"alias": "dien_thoai", "type": "lookup"},\n {"alias": "cccd", "type": "lookup"},\n {"alias": "ngay_sinh", "type": "lookup"},\n {"alias": "dia_chi", "type": "lookup"},\n {"alias": "da_thu", "type": "rollup"},\n {"alias": "chua_thu_phi", "type": "formula"},\n {"alias": "con_no_phi", "type": "formula"},\n {"alias": "vuot_thu_nhap", "type": "formula"},\n {"alias": "da_hoan_tat", "type": "formula"}\n ]\n },\n {\n "alias": "thanh_vien",\n "fields": [\n {"alias": "ho_ten", "type": "text"},\n {"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},\n {\n "alias": "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 {"alias": "ngay_sinh", "type": "date"},\n {"alias": "cccd", "type": "text"},\n {"alias": "thu_nhap", "type": "number", "notation": {"style": "currency", "currency": "VND"}}\n ]\n },\n {\n "alias": "phieu_thu",\n "fields": [\n {\n "alias": "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 {"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},\n {"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {\n "alias": "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 {\n "alias": "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 {"alias": "ngay_thu", "type": "date"},\n {"alias": "nguoi_thu", "type": "select_member"},\n {"alias": "da_chot", "type": "formula"}\n ]\n },\n {\n "alias": "giay_to",\n "fields": [\n {\n "alias": "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 {"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},\n {"alias": "ngay_nhan", "type": "date"},\n {"alias": "can_sua", "type": "text"},\n {"alias": "tep", "type": "files"},\n {"alias": "phai_sua", "type": "formula"}\n ]\n }\n ],\n "records": {\n "ho_so": {\n "title": "khach_hang",\n "subtitle": ["dich_vu"],\n "status": {\n "milestones": [\n {"field": "ngay_thu_phi", "when": {"dich_vu": ["lam_ho_so"]}},\n "ngay_nhan_giay_to",\n {"field": "ngay_lap_ho_so", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_nop_cdt", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_cdt_duyet", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_so_xd_duyet", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_boc_tham", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_ky_hdmb", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_ban_giao", "when": {"dich_vu": ["lam_ho_so"]}}\n ],\n "closed": ["ngay_ky_hdmb", "ngay_ban_giao"]\n },\n "figure": "phi_dich_vu"\n },\n "thanh_vien": {"title": "ho_ten", "subtitle": ["quan_he", "cccd"], "party": "person", "figure": "thu_nhap"},\n "phieu_thu": {\n "title": "khoan",\n "subtitle": ["hinh_thuc", "ngay_thu"],\n "status": {"field": "trang_thai", "closed": ["da_thu", "huy"]},\n "figure": "so_tien"\n },\n "giay_to": {"title": "loai", "image": "tep", "starts": {"ngay_nhan": "today"}}\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 "apps": [\n {\n "alias": "ho_so",\n "name": "H\u1ED3 s\u01A1 nh\xE0 \u1EDF x\xE3 h\u1ED9i",\n "description": "B\xE0n l\xE0m h\u1ED3 s\u01A1 c\u1EE7a t\u1EEBng d\u1EF1 \xE1n: nh\u1EADn gi\u1EA5y t\u1EDD, l\u1EADp v\xE0 n\u1ED9p h\u1ED3 s\u01A1 cho ch\u1EE7 \u0111\u1EA7u t\u01B0, theo t\u1EEBng kh\xE2u duy\u1EC7t t\u1EDBi b\u1ED1c th\u0103m v\xE0 k\xFD h\u1EE3p \u0111\u1ED3ng \u2014 ho\u1EB7c l\xE0m h\u1ED3 s\u01A1 r\u1ED3i b\xE0n giao cho kh\xE1ch t\u1EF1 n\u1ED9p.",\n "icon": "folder-open",\n "theme": {"color": "blue"},\n "entity": "ho_so",\n "scope": "du_an",\n "register": {\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 "export": true,\n "readings": [\n {"metric": "ho_so", "where": {"can_bo_sung": true}, "label": "Ch\u1EDD kh\xE1ch b\u1ED5 sung"},\n {"metric": "ho_so", "value": "da_thu", "over": "ngay_nhan_giay_to", "label": "Ph\xED \u0111\xE3 thu"},\n {"breakdown": "ho_so", "by": "doi_tuong"}\n ]\n },\n "record": {\n "sections": [\n {\n "title": "Ng\u01B0\u1EDDi \u0111\u1EE9ng \u0111\u01A1n",\n "fields": [\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 ],\n "blocks": [\n {\n "rows": "thanh_vien",\n "title": "H\u1ED9 gia \u0111\xECnh",\n "columns": ["ngay_sinh"],\n "create": ["ho_ten", "quan_he", "ngay_sinh", "cccd", "thu_nhap"]\n }\n ],\n "acts": ["doc_giay_to", "giao_xu_ly", "chuyen_du_an"]\n },\n {\n "title": "Nh\xE0 hi\u1EC7n c\xF3",\n "fields": ["giay_chung_nhan_so", "dien_tich_nha"],\n "when": {"tinh_trang_nha_o": ["nha_chat"]}\n },\n {\n "title": "Ph\xED d\u1ECBch v\u1EE5",\n "blocks": [\n {\n "rows": "phieu_thu",\n "title": "Phi\u1EBFu thu",\n "columns": ["so_tien"],\n "create": ["khoan", "so_tien", "hinh_thuc"],\n "where": {"khoan": ["dat_coc", "tat_toan"]},\n "expect": "khoan",\n "when": {"dich_vu": ["tron_goi"]}\n },\n {\n "rows": "phieu_thu",\n "title": "Phi\u1EBFu thu",\n "columns": ["so_tien"],\n "create": ["khoan", "so_tien", "hinh_thuc"],\n "where": {"khoan": ["phi_ho_so"]},\n "expect": "khoan",\n "when": {"dich_vu": ["lam_ho_so"]}\n }\n ]\n },\n {\n "title": "Gi\u1EA5y t\u1EDD",\n "at": ["ngay_thu_phi"],\n "blocks": [\n {\n "rows": "giay_to",\n "title": "Gi\u1EA5y t\u1EDD",\n "columns": ["ngay_nhan"],\n "create": ["loai", "ngay_nhan", "can_sua", "tep"],\n "expect": "loai"\n }\n ]\n },\n {\n "title": "B\u1ED9 h\u1ED3 s\u01A1",\n "description": "C\xE1c m\u1EABu \u0111\u01A1n in t\u1EEB th\xF4ng tin c\u1EE7a h\u1ED3 s\u01A1.",\n "at": ["ngay_nhan_giay_to", "ngay_lap_ho_so"],\n "acts": ["tao_giay_to", "nop_cdt"]\n },\n {\n "title": "Ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "description": "H\u1ED3 s\u01A1 tr\u1ECDn g\xF3i n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0, ch\u1EDD ch\u1EE7 \u0111\u1EA7u t\u01B0 r\u1ED3i S\u1EDF X\xE2y d\u1EF1ng duy\u1EC7t tr\u01B0\u1EDBc ng\xE0y b\u1ED1c th\u0103m.",\n "at": ["ngay_nop_cdt", "ngay_cdt_duyet", "ngay_so_xd_duyet"],\n "fields": ["loai_can"],\n "when": {"dich_vu": ["tron_goi"]}\n },\n {\n "title": "C\u0103n h\u1ED9",\n "at": ["ngay_boc_tham"],\n "fields": ["can_boc", "hoa_hong_sale"],\n "blocks": [{"files": ["hop_dong_mua_ban"]}],\n "when": {"dich_vu": ["tron_goi"]}\n }\n ]\n },\n "acts": [\n {\n "alias": "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 "alias": "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 },\n {\n "alias": "nop_cdt",\n "label": "N\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "when": {"dich_vu": ["tron_goi"]},\n "requires": ["bo_ho_so", "loai_can"],\n "asks": ["loai_can"],\n "set": {"ngay_nop_cdt": "now"},\n "confirm": true\n },\n {\n "alias": "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 "alias": "chuyen_du_an",\n "label": "Chuy\u1EC3n d\u1EF1 \xE1n",\n "asks": [{"input": "du_an_moi", "label": "Sang d\u1EF1 \xE1n", "type": "link", "entity": "du_an", "required": true}],\n "set": {"du_an": "input:du_an_moi"},\n "confirm": true\n },\n {\n "alias": "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 "alias": "xac_nhan_thu",\n "label": "X\xE1c nh\u1EADn \u0111\xE3 thu",\n "of": "phieu_thu",\n "when": {"trang_thai": ["cho_thu"]},\n "asks": ["hinh_thuc"],\n "set": {"trang_thai": "da_thu", "ngay_thu": "now", "nguoi_thu": "me"},\n "confirm": true\n },\n {\n "alias": "huy_phieu",\n "label": "H\u1EE7y phi\u1EBFu",\n "of": "phieu_thu",\n "when": {"trang_thai": ["cho_thu"]},\n "set": {"trang_thai": "huy"},\n "danger": true\n }\n ],\n "checks": [\n {"field": "chua_thu_phi", "blocks": ["nop_cdt"]},\n {"field": "con_no_phi"},\n {"field": "vuot_thu_nhap"},\n {"field": "da_hoan_tat", "blocks": ["delete", "doc_giay_to"]},\n {"field": "da_chot", "of": "phieu_thu", "blocks": ["edit"]},\n {"field": "phai_sua", "of": "giay_to"}\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/papers-and-recordings`, `model/timeline-and-agenda`\n\n**Not when.** Calls made and logged elsewhere \u2014 a `timeline` with its own add keeps the note.\n\n```json\n{\n "entities": [\n {\n "alias": "khach_tiem_nang",\n "fields": [\n {"alias": "ho_ten", "type": "text"},\n {"alias": "ten_cong_ty", "type": "text"},\n {\n "alias": "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 {\n "alias": "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 {\n "alias": "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 {"alias": "so_dien_thoai", "type": "text"},\n {"alias": "zalo", "type": "text"},\n {"alias": "email", "type": "text"},\n {"alias": "trang_ca_nhan", "type": "text", "format": "link"},\n {"alias": "anh", "type": "files"},\n {\n "alias": "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 {\n "alias": "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 {"alias": "vi_sao_du_dieu_kien", "type": "text"},\n {"alias": "phu_trach", "type": "select_member"},\n {"alias": "nguon", "type": "select_record_link", "target_entity": "kenh", "cardinality": "one"},\n {"alias": "lien_he", "type": "select_record_link", "target_entity": "lien_he", "cardinality": "one"},\n {"alias": "cong_ty", "type": "select_record_link", "target_entity": "cong_ty", "cardinality": "one"},\n {"alias": "ngay_chuyen_doi", "type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n {"alias": "ghi_chu", "type": "text", "format": "markdown"},\n {"alias": "tin_hieu", "type": "select_record_link", "target_entity": "tin_hieu"},\n {"alias": "buoc_tiep", "type": "select_record_link", "target_entity": "buoc_tiep"},\n {"alias": "tuong_tac", "type": "select_record_link", "target_entity": "tuong_tac"},\n {"alias": "tin_hieu_moi", "type": "lookup"},\n {"alias": "do_nong", "type": "formula"},\n {"alias": "han_buoc_tiep", "type": "rollup"},\n {"alias": "khong_lien_he", "type": "formula"}\n ]\n },\n {\n "alias": "tin_hieu",\n "fields": [\n {"alias": "tom_tat", "type": "text"},\n {"alias": "noi_dung", "type": "text", "format": "markdown"},\n {"alias": "anh_chup", "type": "files"},\n {"alias": "lien_ket", "type": "text", "format": "link"},\n {"alias": "kenh", "type": "select_record_link", "target_entity": "kenh", "cardinality": "one"},\n {\n "alias": "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 {"alias": "ngay_dang", "type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n {"alias": "ngay_xay_ra", "type": "formula"}\n ]\n },\n {\n "alias": "buoc_tiep",\n "fields": [\n {"alias": "vi_sao_luc_nay", "type": "text"},\n {\n "alias": "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 {"alias": "ban_nhap", "type": "text", "format": "markdown"},\n {"alias": "han", "type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n {\n "alias": "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 },\n {\n "alias": "tuong_tac",\n "fields": [\n {"alias": "tom_tat", "type": "text"},\n {"alias": "thoi_diem", "type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n {\n "alias": "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 {\n "alias": "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 {"alias": "ghi_am", "type": "files"},\n {"alias": "nguyen_van", "type": "text", "format": "markdown"},\n {"alias": "hen_lai", "type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n {"alias": "nguoi_thuc_hien", "type": "select_member"}\n ]\n }\n ],\n "records": {\n "khach_tiem_nang": {\n "title": "ho_ten",\n "subtitle": ["ten_cong_ty"],\n "image": "anh",\n "party": "person",\n "status": {"field": "trang_thai", "closed": ["da_chuyen_doi", "loai"], "history": "lich_su_trang_thai"},\n "due": ["han_buoc_tiep"]\n },\n "tin_hieu": {"title": "tom_tat", "subtitle": ["kenh", "ngay_xay_ra"], "image": "anh_chup"},\n "buoc_tiep": {\n "title": "vi_sao_luc_nay",\n "subtitle": ["viec"],\n "mark": "viec",\n "status": {"field": "tinh_trang", "closed": ["xong", "bo_qua"]},\n "due": ["han"]\n },\n "tuong_tac": {"title": "tom_tat", "subtitle": ["qua", "phan_hoi"]}\n },\n "apps": [\n {\n "alias": "khach_tiem_nang",\n "name": "Kh\xE1ch ti\u1EC1m n\u0103ng",\n "description": "M\u1ECDi ng\u01B0\u1EDDi m\xECnh c\xF3 th\u1EC3 b\xE1n d\u1ECBch v\u1EE5, t\u1EEB t\xEDn hi\u1EC7u \u0111\u1EA7u ti\xEAn \u0111\u1EBFn khi chuy\u1EC3n v\xE0o CRM: m\u1EDF m\u1ED9t kh\xE1ch \u0111\u1EC3 \u0111\u1ECDc v\xEC sao n\xEAn g\u1ECDi l\xFAc n\xE0y, li\xEAn h\u1EC7 ngay, ghi l\u1EA1i l\u1EA7n ch\u1EA1m, x\u1EBFp b\u01B0\u1EDBc ti\u1EBFp theo, v\xE0 chuy\u1EC3n \u0111\u1ED5i khi \u0111\xE3 c\xF3 cu\u1ED9c tr\xF2 chuy\u1EC7n.",\n "icon": "radar",\n "theme": {"color": "violet"},\n "entity": "khach_tiem_nang",\n "register": {\n "columns": ["phu_trach", "han_buoc_tiep", "tin_hieu_moi"],\n "filters": ["phu_trach", "tin_hieu_moi"],\n "create": ["ho_ten", "trang_ca_nhan", "so_dien_thoai", "ten_cong_ty", "nguon"],\n "readings": [\n {"breakdown": "khach_tiem_nang", "by": "phu_trach"},\n {"trend": "khach_tiem_nang", "over": "ngay_chuyen_doi", "label": "Kh\xE1ch chuy\u1EC3n \u0111\u1ED5i"}\n ]\n },\n "record": {\n "sections": [\n {\n "title": "\u0110\xE1nh gi\xE1",\n "at": ["moi"],\n "fields": ["vai_tro", "loai_hinh", "quy_mo", "vi_sao_du_dieu_kien", "do_nong"],\n "blocks": [\n {\n "rows": "tin_hieu",\n "title": "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 "acts": ["danh_gia_dat"]\n },\n {\n "title": "Li\xEAn h\u1EC7",\n "at": ["du_dieu_kien"],\n "fields": ["so_dien_thoai", "phu_trach", "trang_ca_nhan", "zalo", "email", "ghi_chu"],\n "blocks": [\n {\n "rows": "buoc_tiep",\n "title": "B\u01B0\u1EDBc ti\u1EBFp theo",\n "columns": ["ban_nhap"],\n "create": ["vi_sao_luc_nay", "viec", "han", "ban_nhap"]\n },\n {\n "timeline": "tuong_tac",\n "title": "T\u01B0\u01A1ng t\xE1c",\n "create": ["tom_tat", "qua", "phan_hoi", "thoi_diem", "hen_lai"]\n }\n ],\n "acts": ["ghi_cuoc_goi", "da_lien_he"]\n },\n {\n "title": "Chuy\u1EC3n \u0111\u1ED5i",\n "description": "Kh\xE1ch \u0111\xE3 tr\u1EA3 l\u1EDDi v\xE0 mu\u1ED1n \u0111i ti\u1EBFp: chuy\u1EC3n \u0111\u1ED5i t\u1EA1o li\xEAn h\u1EC7 v\xE0 c\xF4ng ty trong s\u1ED5 kh\xE1ch h\xE0ng.",\n "at": ["da_lien_he"],\n "fields": ["nguon", "lien_he", "cong_ty"],\n "acts": ["chuyen_doi"]\n },\n {\n "title": "Lo\u1EA1i",\n "description": "Kh\xE1ch kh\xF4ng \u0111i ti\u1EBFp: ghi l\xFD do \u0111\u1EC3 l\u1EA7n sau kh\xF4ng t\xECm l\u1EA1i.",\n "at": ["loai"],\n "fields": ["ly_do_loai"],\n "acts": ["loai"]\n }\n ]\n },\n "acts": [\n {\n "alias": "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 "alias": "ghi_cuoc_goi",\n "label": "Ghi \xE2m cu\u1ED9c g\u1ECDi",\n "when": {"trang_thai": ["du_dieu_kien"]},\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 "fills": ["tom_tat", "phan_hoi"]\n },\n {\n "alias": "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 "alias": "chuyen_doi",\n "label": "Chuy\u1EC3n \u0111\u1ED5i",\n "when": {"trang_thai": ["da_lien_he"]},\n "workflow": "chuyen_doi_khach",\n "writes": ["lien_he", "cong_ty"],\n "confirm": true\n },\n {\n "alias": "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 "alias": "lam_xong",\n "label": "\u0110\xE3 l\xE0m",\n "of": "buoc_tiep",\n "when": {"tinh_trang": ["cho"]},\n "set": {"tinh_trang": "xong"}\n }\n ],\n "checks": [{"field": "khong_lien_he", "resolve": "da_lien_he"}]\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 `register.readings` leading with the headline number.\n\n**Keys.** `model/status`, `model/record-page`, `model/readings`\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 "entities": [\n {\n "alias": "de_nghi",\n "fields": [\n {"alias": "noi_dung", "type": "text"},\n {"alias": "so_de_nghi", "type": "text"},\n {\n "alias": "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 {"alias": "du_an", "type": "select_record_link", "target_entity": "du_an", "cardinality": "one"},\n {\n "alias": "nha_cung_cap",\n "type": "select_record_link",\n "target_entity": "nha_cung_cap",\n "cardinality": "one"\n },\n {"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {"alias": "han_thanh_toan", "type": "date"},\n {\n "alias": "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 {"alias": "nguoi_de_nghi", "type": "select_member"},\n {"alias": "chung_tu", "type": "files"},\n {"alias": "nguoi_duyet", "type": "select_member"},\n {"alias": "duyet_luc", "type": "date", "format": "datetime"},\n {"alias": "ly_do_tu_choi", "type": "text"},\n {"alias": "phieu_chi", "type": "files"},\n {"alias": "ngan_sach_du_an", "type": "lookup"},\n {"alias": "da_duyet_du_an", "type": "lookup"},\n {"alias": "vuot_ngan_sach", "type": "formula"},\n {"alias": "sat_ngan_sach", "type": "formula"},\n {"alias": "da_gui", "type": "formula"}\n ]\n }\n ],\n "records": {\n "de_nghi": {\n "title": "noi_dung",\n "subtitle": ["du_an"],\n "status": {\n "field": "trang_thai",\n "closed": ["da_chi", "tu_choi"],\n "history": "lich_su_duyet",\n "stages": {\n "nhap": {"owner": "nguoi_de_nghi"},\n "cho_duyet": {"owner": {"role": "giam_doc"}, "within": 1},\n "cho_chu_tich": {"owner": {"role": "chu_tich"}, "within": 2},\n "da_duyet": {"owner": {"role": "ke_toan"}, "within": 2}\n }\n },\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 }\n },\n "apps": [\n {\n "alias": "de_nghi_chi",\n "name": "\u0110\u1EC1 ngh\u1ECB thanh to\xE1n",\n "description": "Ch\u1EC9 huy tr\u01B0\u1EDFng l\u1EADp \u0111\u1EC1 ngh\u1ECB chi cho d\u1EF1 \xE1n c\u1EE7a m\xECnh, k\xE8m ch\u1EE9ng t\u1EEB, r\u1ED3i g\u1EEDi gi\xE1m \u0111\u1ED1c duy\u1EC7t.",\n "icon": "hand-coins",\n "theme": {"color": "amber"},\n "entity": "de_nghi",\n "register": {\n "columns": ["nguoi_de_nghi"],\n "filters": ["nguoi_de_nghi", "du_an"],\n "group": "du_an",\n "create": ["noi_dung", "loai", "du_an", "nha_cung_cap", "so_tien", "han_thanh_toan", "nguoi_de_nghi"],\n "readings": [\n {\n "metric": "de_nghi",\n "value": "so_tien",\n "where": {"trang_thai": ["da_duyet"]},\n "label": "\u0110\xE3 duy\u1EC7t, ch\u1EDD chi"\n },\n {"trend": "de_nghi", "over": "han_thanh_toan", "value": "so_tien"}\n ]\n },\n "record": {\n "door": "drawer",\n "sections": [\n {\n "title": "\u0110\u1EC1 ngh\u1ECB",\n "at": ["nhap"],\n "fields": ["so_de_nghi", "loai", "nha_cung_cap", "han_thanh_toan", "nguoi_de_nghi"],\n "blocks": [{"files": ["chung_tu"], "title": "Ch\u1EE9ng t\u1EEB k\xE8m"}],\n "acts": ["gui_duyet"]\n },\n {\n "title": "Duy\u1EC7t",\n "at": ["cho_duyet", "cho_chu_tich"],\n "fields": ["nguoi_duyet", "duyet_luc", "ly_do_tu_choi"],\n "acts": ["rut_lai"]\n },\n {"title": "\u0110\xE3 chi", "at": ["da_chi"], "blocks": [{"files": ["phieu_chi"], "title": "Phi\u1EBFu chi"}]}\n ]\n },\n "acts": [\n {\n "alias": "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 "alias": "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 "checks": [{"field": "da_gui", "blocks": ["edit"]}, {"field": "vuot_ngan_sach"}, {"field": "sat_ngan_sach"}]\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.** `dashboard`: `metric`s with `over` (the period windows them), a `trend`, `breakdown`s, and a `list` of rows to act on opening in their app.\n\n**Keys.** `model/dashboards`, `model/readings`\n\n**Not when.** One job\'s headline numbers \u2014 those lead that job\'s register as its `readings`.\n\n```json\n{\n "entities": [\n {\n "alias": "giao_dich",\n "fields": [\n {"alias": "noi_dung", "type": "text"},\n {"alias": "ngay", "type": "date"},\n {\n "alias": "loai",\n "type": "select",\n "options": [{"alias": "thu", "color": "green"}, {"alias": "chi", "color": "rose"}]\n },\n {"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {"alias": "bien_dong", "type": "formula"},\n {\n "alias": "doi_tuong",\n "type": "select_record_link",\n "target_entity": "doi_tuong",\n "cardinality": "one"\n },\n {\n "alias": "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 {"alias": "sao_ke", "type": "select_record_link", "target_entity": "sao_ke", "cardinality": "one"},\n {"alias": "hoa_don", "type": "select_record_link", "target_entity": "hoa_don", "cardinality": "many"},\n {\n "alias": "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 ]\n },\n {\n "alias": "hoa_don",\n "fields": [\n {\n "alias": "doi_tuong",\n "type": "select_record_link",\n "target_entity": "doi_tuong",\n "cardinality": "one"\n },\n {"alias": "so_hoa_don", "type": "text"},\n {"alias": "ngay_hoa_don", "type": "date"},\n {"alias": "so_ngay_qua_han", "type": "formula"},\n {\n "alias": "chieu",\n "type": "select",\n "options": [{"alias": "ban_ra", "color": "green"}, {"alias": "mua_vao", "color": "violet"}]\n },\n {"alias": "tong_cong", "type": "formula"},\n {"alias": "thue_suat", "type": "formula"},\n {"alias": "tep", "type": "files"},\n {\n "alias": "giao_dich",\n "type": "select_record_link",\n "target_entity": "giao_dich",\n "cardinality": "many"\n }\n ]\n }\n ],\n "records": {\n "giao_dich": {\n "title": "noi_dung",\n "subtitle": ["ngay"],\n "status": {"field": "trang_thai", "closed": ["da_khop", "khong_can_hd"], "history": "lich_su_khop"},\n "figure": "bien_dong"\n },\n "hoa_don": {"title": "doi_tuong", "subtitle": ["so_hoa_don", "ngay_hoa_don"], "image": "tep", "figure": "tong_cong"}\n },\n "apps": [\n {\n "alias": "dong_tien",\n "name": "D\xF2ng ti\u1EC1n",\n "description": "Gi\xE1m \u0111\u1ED1c xem trong k\u1EF3: ti\u1EC1n v\xE0o, ti\u1EC1n ra, d\xF2ng ti\u1EC1n r\xF2ng qua c\xE1c k\u1EF3, chi v\xE0o \u0111\xE2u, thu t\u1EEB ai, v\xE0 nh\u1EEFng d\xF2ng sao k\xEA c\xF2n ch\u01B0a kh\u1EDBp.",\n "icon": "chart-line",\n "theme": {"color": "emerald"},\n "dashboard": [\n {\n "metric": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "over": "ngay",\n "label": "Ti\u1EC1n v\xE0o"\n },\n {\n "metric": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "over": "ngay",\n "better": "down",\n "label": "Ti\u1EC1n ra"\n },\n {"trend": "giao_dich", "over": "ngay", "value": "bien_dong", "label": "D\xF2ng ti\u1EC1n r\xF2ng"},\n {\n "breakdown": "giao_dich",\n "by": "danh_muc",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "over": "ngay",\n "label": "Chi theo danh m\u1EE5c"\n },\n {\n "breakdown": "giao_dich",\n "by": "doi_tuong",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "over": "ngay",\n "label": "Thu theo kh\xE1ch h\xE0ng"\n },\n {\n "pivot": "hoa_don",\n "rows": "doi_tuong",\n "columns": {"field": "so_ngay_qua_han", "tiers": [1, 8, 16, 31]},\n "value": "tong_cong",\n "where": {"chieu": ["ban_ra"]},\n "label": "C\xF4ng n\u1EE3 b\xE1n ra theo tu\u1ED5i n\u1EE3"\n },\n {\n "metric": "hoa_don",\n "value": "so_ngay_qua_han",\n "average": true,\n "where": {"chieu": ["ban_ra"]},\n "label": "S\u1ED1 ng\xE0y qu\xE1 h\u1EA1n b\xECnh qu\xE2n"\n },\n {"metric": "hoa_don", "value": "thue_suat", "average": true, "label": "Thu\u1EBF su\u1EA5t b\xECnh qu\xE2n"},\n {\n "list": "giao_dich",\n "where": {"trang_thai": ["chua_khop"]},\n "columns": ["sao_ke"],\n "app": "doi_chieu",\n "label": "D\xF2ng ch\u01B0a kh\u1EDBp"\n }\n ]\n }\n ]\n}\n```\n';
|
|
44539
44512
|
|
|
44540
44513
|
// AGENTS.md
|
|
44541
|
-
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>` | How to write a `model.json` \u2014 the file a workspace is built from: its tables, how a row of each is recognised (`records`), and the apps stated over them. Every top-level key, every field type with the config it needs, the row format, the rules, and a worked example; complete example models of several trades are listed at `https://lotics.ai/presets/index.json`. `lotics model apply` checks the file (every problem in one run), applies its tables and mints a version of each app; the WORKSPACE remembers what each alias became, so every later apply binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics model pull` goes the other way \u2014 the workspace's model, rebuilt from what owns each part. |\n| `lotics docs design` \xB7 `lotics docs design/<section>` | Designing an app \u2014 where an app starts: the method, the treatment each kind of row takes with the `model` page stating its keys, the visual bar, and looking at what an apply built. A finding of the apply names its section. |\n| `lotics docs examples` \xB7 `lotics docs examples/<treatment>` | Each treatment worked through in an app of a complete model: the job, the keys to read, where it is the wrong one, and the excerpt. |\n| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE \u2014 clarify, model, apply or build, prove, look \u2014 for an app stated in a model 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 entity per table 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 every field type, its properties, computed fields, and what `update_fields` takes. 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 lifecycle workflows. |\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. |\n| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the workspace facts an agent can't guess; who can read a doc; catalog-then-stage retrieval. |\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 `model apply`,\n `model check`, `model pull`, `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 model --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 setup` falls into the same\n flow by itself when the email it was given already has an account: it stops having created\n nothing, and the SAME command run again carries on.\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 applying or pulling a model\n (`model apply`, `model pull`, `setup`), 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_agent`, `run_app_query`) and every one that changes an app\n (`set_app_queries`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command\n exists only for work that touches a local file.\n- **Every change to an app mints a version of it, and rolling back is the undo.** An apply, a\n deploy, a single `set_app_*` or `remove_app_binding`: each is a new version, and `rollback_app` makes an\n earlier one current again. It 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` or `run_app_agent` result's own envelope carries a failed\n `status` (`error`/`failed`/`cancelled`) \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 in a model is changed by applying the\n model 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";
|
|
44514
|
+
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>` | How to write a `model.json` \u2014 the file a workspace is built from: its tables, how a row of each is recognised (`records`), and the apps stated over them. Every top-level key, every field type with the config it needs, the row format, the rules, and a worked example; complete example models of several trades are listed at `https://lotics.ai/presets/index.json`. `lotics model apply` checks the file (every problem in one run), applies its tables and mints a version of each app; the WORKSPACE remembers what each alias became, so every later apply binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics model pull` goes the other way \u2014 the workspace's model, rebuilt from what owns each part. |\n| `lotics docs design` \xB7 `lotics docs design/<section>` | Designing an app \u2014 where an app starts: the method, the treatment each kind of row takes with the `model` page stating its keys, the visual bar, and looking at what an apply built. A finding of the apply names its section. |\n| `lotics docs examples` \xB7 `lotics docs examples/<treatment>` | Each treatment worked through in an app of a complete model: the job, the keys to read, where it is the wrong one, and the excerpt. |\n| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE \u2014 clarify, model, apply or build, prove, look \u2014 for an app stated in a model 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 entity per table 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 every field type, its properties, computed fields, and what `update_fields` takes. 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. |\n| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the workspace facts an agent can't guess; who can read a doc; catalog-then-stage retrieval. |\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 `model apply`,\n `model check`, `model pull`, `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 model --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 setup` falls into the same\n flow by itself when the email it was given already has an account: it stops having created\n nothing, and the SAME command run again carries on.\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 applying or pulling a model\n (`model apply`, `model pull`, `setup`), 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_agent`, `run_app_query`) and every one that changes an app\n (`set_app_queries`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command\n exists only for work that touches a local file.\n- **Every change to an app mints a version of it, and rolling back is the undo.** An apply, a\n deploy, a single `set_app_*` or `remove_app_binding`: each is a new version, and `rollback_app` makes an\n earlier one current again. It 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` or `run_app_agent` result's own envelope carries a failed\n `status` (`error`/`failed`/`cancelled`) \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 in a model is changed by applying the\n model 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";
|
|
44542
44515
|
|
|
44543
44516
|
// docs/building_an_app.md
|
|
44544
44517
|
var building_an_app_default = '# Building an app, end to end\n\nThe other references here describe **contracts** \u2014 what a model 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 model, deploying a\nbuild, setting one query or workflow \u2014 mints a new version of it, and rolling back to an earlier\nversion is the undo. Nothing about an app lives in a local directory the platform reads back.\n\n**Rolling back restores the app, not the data.** A table change the model made, and every row a\nworkflow wrote while you 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 in a model** \u2014 the default, and the right one for almost every job. `model.json`\n says how a row of each entity is recognised (`records`) and, in `apps`, one register over one\n entity and the record its rows open: 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/register`,\n `model/record-page`, `model/acts`, `model/checks`). 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 key changes it. Every write the app makes is a\n generated workflow that re-checks on the server what the model states.\n- **A custom-code app** \u2014 React you write, for a surface the model\'s vocabulary 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 modelling 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 entity 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 entities 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 entity 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**One fact, one column \u2014 and check before you add one.** Read the table\'s existing fields before\nadding any, because the fact is often already there in another shape: a place written as text\nbeside a link to the place record, a status word beside the select that decides it, a total beside\nthe formula that computes it. Two columns for one fact never stay equal. Prefer the link, the\nselect, or the formula, and compose the text when you READ.\n\n**Match on ids and option keys, never on rendered text.** Resolve a name to its `rec_\u2026` or `opt_\u2026`\nonce, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.\n\n**How the tables RELATE is `lotics docs data_model`** \u2014 read it before designing a schema; those\ndecisions outlive any one app.\n\n**Empty is not the same as redundant.** A field nothing fills may still be the only home for a real\ndistinction. Read what a field MEANS before you remove it.\n\n## 4 \u2014 An app stated in a model\n\n```\nlotics docs model # how to write model.json, with a worked example\nlotics docs design # how to design each app: the method, a treatment per kind of row\nlotics model apply model.json # check it, apply the tables, mint a version of every app\nlotics model apply model.json --app orders # only the apps named; the tables are applied whole\nlotics model check model.json # what the apply would change, writing nothing\nlotics model pull -o model.json # the workspace\'s model, as the file apply reads\n```\n\n`model apply` checks the whole file first \u2014 every problem in one run, before anything is uploaded\nor written. It then adopts or creates each table (the table this workspace bound it to, else one of the same\nlabel, is adopted and given what it lacks; no stored value changes), writes the first rows only where every bound\ntable is empty, and mints one version per app, printing each app\'s id, the version minted\n(`unchanged` when there was nothing to mint) and its address. Applying the same file again mints nothing.\nA table change is not undone by rolling an app back, so `lotics model check` first says what the apply would\ncreate or change in the tables and which apps it would create, update or refuse \u2014 writing nothing.\n\n**An app leaving out a treatment its rows call for is refused** \u2014 readings over its rows, a record\'s\nsections, an act per status move, a picture where its rows hold photos (`lotics docs design`). Each\nrefusal names the rule, and beside them comes each refused app\'s patch adopting its decisions: merge\nthe patches into the file in the order given, or state why the app stays as it is under the\n`declines` key the refusal names (`lotics docs model/declines`), then apply again. `lotics model check` reads the\ndecisions beside the workspace\'s rows, and with `--json` gives each app as the patches leave it, its\n`draft`.\n\n**The model changes after the app exists, and applying it again is how it lands.** Edit the file,\napply it. An act whose write the model cannot say names its own `workflow`; that workflow\'s body 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 the app declares in `workflows`\n(`lotics docs model/apps`): the model states what it takes and hands back, `lotics run\nset_app_workflow` sets its body with `inputs` and `outputs` left out, and every apply keeps that\nbody. A workflow set on the app that its model declares nowhere is archived by the next apply. An act that reads papers runs an agent\napply owns: made, replaced and removed with the act, and an agent of that alias apply did not make\nis refused, never rewritten.\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": "fld_\u2026", "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.** Every workflow bound to an app is also the chat\nagent\'s write surface, so its shape is an agent-facing decision: take a list where one job covers\nmany records, say in an optional input\'s `description` what omitting it means, make the write\nsurvive running twice, and gate anything irreversible with `wait_for_approval`.\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 dry_run_workflow \'{"trigger_type":"app_workflow","trigger_payload":{\u2026},"live_reads":true}\'\n```\n\nIt walks the real step tree and returns the resolved plan plus expression and tool-input errors,\ndispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it\nevery read returns a stub, and every data-gated branch takes the empty path.\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 run rollback_app` away from the one before.\n\nFor an app a model applied, `verdict` comes beside the images: what a check of the workspace\'s\nmodel finds of that app now, beside its rows \u2014 what the next apply would refuse it for and the notes\non it, the `patch` adopting the decisions among them, and the app\'s body as the patch\nleaves it (`draft`). `verdict_unread` says why none was read.\n\nThe app\'s `name`, `icon`, `theme` and `description` are keys of the app in the model, and each apply\nwrites them onto the app; one the model leaves out is cleared. `lotics run update_app` changes them on\nthe app, and `lotics model pull` reads them back as the app holds them. 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';
|
|
@@ -44547,7 +44520,7 @@ var building_an_app_default = '# Building an app, end to end\n\nThe other refere
|
|
|
44547
44520
|
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 model --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| `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_agent`, `run_app_query`) and every one that changes an app (`set_app_queries`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command exists only for work that touches a local file: `model apply`, `model check`, `model pull`, `app create --custom`, `app pull`, `app deploy`. |\n| \u2014 | **The exit code reports the WORK, not just the call \u2014 for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) 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 <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_key> [-o <dir>]` 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]` | `GET /v1/knowledge_docs` \u2014 a table of id, name, tags, description (`--json` for the docs). **REST, not the `list_knowledge` tool**: 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)`. |\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 `create_knowledge` with `{ name, description, content, tags? }` (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 content-read path for a non-sandbox client). `-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 `update_knowledge` with **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, `list_knowledge`, and the corpus `grep_knowledge` 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>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |\n| `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes \u2014 `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** \u2014 it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as \u2014 that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created \u2014 the design decisions an apply would refuse it for too (`lotics docs design`), since a new workspace holds no rows to change them \u2014 because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name \u2014 the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning \u2014 `setup` writes nothing to disk \u2014 so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** \u2014 what `lotics model apply --json` does (`tables`; `apps`, each with `alias`, `app_id`, `version_id`, `origin`, `address` and `findings`; and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup \u2026`. |\n| `lotics model apply <model.json> [--app <alias> ...] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked against the model's own rules with the validator the server runs, every problem in one run, before anything is uploaded. **The apply refuses an app leaving out a treatment its rows call for** (`lotics docs design`) \u2014 judged by the server beside the workspace's rows, before it writes anything \u2014 until the app adopts it or states why not under the `declines` key the refusal names; the refusal carries each refused app's patch adopting its decisions, to merge in the order printed. Where the model's own rules pass, one refusal carries everything decidable before the first write \u2014 those decisions, what compiling each app finds, a field that cannot leave its table and what the caller may not change \u2014 so one pass fixes them. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. **Where the file was pulled (`-o`) or applied in this workspace before, only what it changed since is sent**, as a `patch`: what changed in the workspace since and the file does not touch stays. An item named by its `alias` that the file puts in a new place \u2014 an act among an app's acts, a field between two others \u2014 is sent as a move beside the patch: inside an app it is built into the app, an option of a select takes its new place among that select's options on the table, and a table's fields and views, the roles, the templates and the apps themselves keep the order the workspace holds, said as a note. A list whose items carry no `alias` \u2014 an app's columns, a section's fields, the sections \u2014 is sent whole in its new order. What the file takes out is sent as a removal: it goes where an apply rebuilds it (an app's acts and columns, a write rule), a field leaves its table, and it is refused, naming the tool that deletes it, where an apply never deletes it (a table, an option, a view, an app). **A field the file no longer states leaves its table** \u2014 a field an earlier apply stated, or one the pulled file stated \u2014 only where no live row holds a value in it and nothing outside the model's own apps reads it: no formula, rollup or lookup, no figure's unit or currency, no `default_from` copy, no unique set, no query, agent, table check or workflow body of the workspace. Otherwise nothing is applied and the refusal names the field, how many rows hold a value and what reads it; a workflow body the author wrote on an app the apply rebuilds that still names the field keeps it on the table, said in the apply's findings. A field of the table no model ever stated is left as it is. **A label the file changes is a rename**: a field's and a select option's label, and a table's description, are written onto the table as the file states them, each named in the notes. **A declared workflow whose `inputs` or `outputs` the file changes** keeps the body set on it where the body still meets them; where it does not, the apply lands and the workflow stands on no body again \u2014 every call refused \u2014 until `set_app_workflow` sets one that does, said on the app's line. A file stating a `null` a patch would read as a removal sends the whole model, said on stderr, and so does a reorder sent to a server that takes no move. A patch builds the apps it states or moves an item of and each other app whose build it changes, `--app` naming others; one whose every key and move lies inside one app's body leaves the tables as they stand \u2014 its `tables` is empty \u2014 and checks and builds that app alone, beside any other app whose build reads what it changed. The copy each change is read against is kept per workspace and file under `~/.lotics/model_bases`: an apply narrowed by `--app` leaves in it the other apps as they were, so the next apply sends their edits again, and an apply that fails \u2014 refused, or cut off \u2014 leaves none, so the next sends the whole model. Then the tool adopts or creates every table (the table this workspace bound the entity to, else an existing table of the same label, is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares, or the patch builds \u2014 `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app it built \u2014 alias, `app_id`, `created`/`updated` with the version minted or `unchanged`, and the address it is served at \u2014 then the model's notes once, as the server states them. **What it would change, writing nothing, is `lotics model check`.** **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` on stdout, or `{ok: false, findings}` when the file does not check. |\n| `lotics model check <model.json> [--app <alias> ...] [--json]` | **What applying the model would change, through the `check_model` tool, writing nothing.** The file is read and checked as `model apply` checks it, then sent the same way \u2014 whole, or as what it changed since its base \u2014 and nothing is uploaded or kept. It prints what the apply would do to the schema (what it would add, each kind counted \u2014 tables, fields, options, constraints \u2014 what it leaves as the workspace has it, what the two disagree about), which apps it would create or update, what it would refuse an app for \u2014 the decisions among its findings, read beside the workspace's rows, with each refused app's patch \u2014 and what it would refuse of the model beside its apps (a field that cannot leave its table, a template nothing stores), all in the one run, exiting 1 when it would refuse anything; its notes name each field it would take off a table and each label it would rename; workflow bodies are checked only at apply, since they name fields a check has not created. `check_model` only reads, so a key holding read access alone runs this where `model apply` is refused, and so does a private instance whose licence admits reads only: it needs `schema:read`, and `data:read` when the file states an app or is sent as what it changed. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` \u2014 each table and app what the apply would do, an app the patches change carrying its body as they leave it as `draft` \u2014 and `designs`, each refused app's patch in merge order, or `{ok: false, findings}` when the file does not check. |\n| `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** \u2014 the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version \u2014 through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. What the workspace holds that a model cannot state is printed on stderr, never written into the file. Applying what it wrote changes nothing. With `-o`, what it wrote is the copy the next `model apply` of that file reads its changes against. |\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_app_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: an app the runtime draws from a model is made by `lotics model apply`. The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |\n| `lotics app pull [app_id] [path]` | **A custom-code app's 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_app_types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). A JSON app has no source tree: the server's refusal names `get_model`, and `lotics model pull` is its pull. |\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_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent) 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`, with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` \u2014 the dialect `grep_knowledge` reads \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 model reference, from inside the binary** \u2014 it describes this CLI's own model checker, at this CLI's version; `lotics docs` lists it under \"Build an app\", after `design`. Its first page is what a model composes with, the working order (jobs \u2192 entities and fields \u2192 `records` \u2192 one app per job \u2192 `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `\"<entity-alias>:<ref>\"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **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";
|
|
44548
44521
|
|
|
44549
44522
|
// docs/data_model.md
|
|
44550
|
-
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, and add nothing that stores a fact the table already\nstores.** A value written as text and the same value held as a `select_record_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\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\nWorth writing as a test rather than a note, because a note about a condition nobody re-checks goes\nstale in silence.\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 \u2014 keys\nare minted per field. So anything moving a value between them needs a hand-written key map.\n\nThat map is code. Put it in one named module with a test; written inline at the copy it is invisible,\nuntested, and silently wrong the first time somebody renames an option, because a rename leaves the\nkey intact and the map still compiling. Prefer a link to a shared reference table where the set is\nopen or growing; keep a map only for a small closed set.\n\nWhere one side genuinely holds MORE values than the other, that is not drift \u2014 it is the model\ntelling the truth. The wider side must **refuse** what the narrower one cannot express rather than\nquietly picking the nearest value.\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\nDeclare the natural key in the table\'s description.\n\nAnything that imports, reconciles or de-duplicates has to decide identity, and with no declared key\nit falls back to comparing displayed text \u2014 which is how one company arrives three times under three\nspellings. Name the key: a reference number, a tax id, a link plus a period. Then match on `rec_\u2026`\nand `opt_\u2026`, never on rendered labels.\n\n## A state\'s HISTORY is rows, not columns\n\nA `changed at` column says only how long a row has been where it is now \u2014 the next move overwrites\nit \u2014 and a date column per state holds until something re-enters a state it already left.\n\nMeasuring time-in-state or conversion needs one ROW per move: a link to the subject, the state left,\nthe state entered, when. Hold those states as the source field\'s own `opt_` keys so the log carries\nno second vocabulary, and write the rows from that table\'s own lifecycle workflows, which covers\nevery writer rather than one app\'s.\n\n## Keep derived chains shallow\n\nFormulas and rollups are computed and STORED when a row is written, and one that reads another\nrecomputes with it. A rollup over a formula over a formula is paid three times on every touch, and\nagain for every row upstream of it.\n\n**Depth costs more than row count.** This is the optimisation lever that actually exists here; row\nscanning is the platform\'s problem, chain depth is yours.\n\n## Changing a money formula on a live table\n\nFormula edits recompute every row, so the only honest proof that one changed nothing it should not is\nthe numbers themselves:\n\n1. Snapshot the affected totals to a file.\n2. Make the change.\n3. Diff. Identical is the pass.\n\nAnd when a formula gains a new field, **test that the value IS the one you want, never that it\ndiffers from it** \u2014 an empty cell reads as `""`, which differs from every option key, so the inverted\nspelling silently zeroes every row written before the field existed.\n\n## Fields\n\nWhat `create_table` (on the CLI or in chat) and `update_table` take in `add_fields`, and `update_table` in `update_fields`.\n\n### Types and formats\n\n`type` is one of `text`, `number`, `date`, `boolean`, `select`, `select_member`,\n`select_record_link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`. `button` is retired: an\nexisting button field keeps running, but none is created, converted to or edited.\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### Properties\n\nA type\'s properties sit **directly on the field object** \u2014 there is no `config` wrapper. A property\nmay itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`); its inner keys stay\ninside it. Each type takes only its own:\n\n- **text** \u2014 `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).\n- **number** \u2014 `notation?`, `default_value?` (a number). `notation` is how the figure reads, one\n of three; on `update_fields` a stated one replaces the field\'s whole.\n - `{ style: "decimal" }` \u2014 a plain number, as a field stating none reads.\n - `{ style: "currency", currency: "VND" }` \u2014 money in an ISO 4217 code.\n - `{ style: "unit", unit }` \u2014 a quantity: `"percent"` (10 reads "10%", never multiplied), a\n measured code \u2014 g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day \u2014 or a counted noun such\n as `ki\u1EC7n`. A change between two units of one dimension converts every stored figure; any other\n change relabels.\n - A currency or unit read on each row is `{ per_row: "\u0110VT" }` in its place: a single select on\n the same row, or a lookup of one through a one-link, named by name or key, whose chosen option\n is that row\'s currency or unit \u2014 every option label an ISO 4217 code, or a unit as `unit`\n takes one.\n- **date** \u2014 `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,\n `derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row\'s\n creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no\n `default_value`, and holds only the `date` and `datetime` formats.\n- **boolean** \u2014 `default_value?` (`true` | `false`).\n- **select** \u2014 `options` `[{ name, color?, mark? }]`, `multi?`, `default_value?`.\n- **select_member** \u2014 `multi?`, `default_value?` (member ids).\n- **select_record_link** \u2014 `table_id`, `display_field_keys?`, `sync_both_ways?`, `cardinality?`,\n `paired_field_name?`, `paired_field_display_field_keys?`. Without `display_field_keys` a link shows\n the target\'s autonumber, else its first text field. `sync_both_ways: true` on an existing one-way\n link makes it two-way \u2014 never add a second link for that. `cardinality: "one"` marks the child\n side of a parent-child pair (the linked record is this one\'s parent); two paired sides cannot both\n be `"one"`. The `paired_*` keys name and label the back-reference the target gets.\n- **files** \u2014 no properties.\n- **autonumber** \u2014 `template` (`"INV-{YEAR}-{N:4}"`; tokens `{N}`, `{N:W}` zero-padded to W,\n `{YEAR}`, `{YEAR:2}`, `{MONTH}`, `{DAY}`), or `prefix` + `padding` (1\u201320) for `PREFIX-0001`.\n- **any type**, at create \u2014 `confirm_before_update?`: a person confirms each later edit.\n\n`default_value` fills the field on a NEW record that states no value; existing records are never\nbackfilled, and null clears it. A select\'s or a member field\'s default is an ARRAY even when the\nfield holds one (`["Unread"]`, never `"Unread"`) \u2014 option names in `add_fields`, where the keys do\nnot exist yet, and option keys (`opt_\u2026`) in `update_fields`.\n\n`default_from` (text, number, date, boolean, select, select_member, select_record_link) copies a\nvalue onto a NEW record off the row one of its links names: `{ link_field_key, field_key,\noptions? }`, the link of cardinality `"one"` and not itself copied, the source a field of the linked\ntable of this field\'s own type, date format and notation (a link one to the same table) \u2014 a formula,\na rollup, or a lookup through a one-row link (or an ordered one) read as the value it holds; several\nvalues only into a field holding several; never into a date `derive_from` stamps. It is\ncopied when the record is made and never re-read, so a later change to the source leaves the record\nalone (a line\'s price as it was when the line was booked). A value the write states wins, then the\ncopy, then `default_value`. A select source\'s `options` maps its option keys onto this field\'s\n(`update_fields`, once the keys exist); an option left unmapped is not copied. null clears it. Deleting the link a\ncopy reads through, or the field it copies off, is refused even with `force` until the copy is\ncleared; the dependency report lists the copying field as `copy_link` or `copy_source` under\n`requires_manual_action`. Any other schema change that leaves a copy unable to stand \u2014 its source\nretyped, its link made to hold many \u2014 is refused, naming the copy.\n\n### Computed fields\n\nRead-only in records. `formula` is one key holding an object; a rollup\'s and a lookup\'s properties\nare separate keys on the field \u2014 `rollup: {\u2026}` and `lookup: {\u2026}` are refused as unrecognized.\n\nformula: { expression, notation?, link? }\n\n- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never\n breaks the formula. A reference to no field, or a call to something that is no helper, is refused.\n- A select reads as an ARRAY of option keys: `{Status}[0]` for a single select,\n `includes({Tags}, "opt_\u2026")` for a multi.\n- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` \u2014 a model\n names fields by alias where a table tool names them by name or key; the language is the same.\n- `notation` is how a number result reads, as a number field\'s; `link: true` says the result is a\n URL, drawn as a link that opens.\n- On `update_fields` a key left out keeps its stored value, and a stated `notation` replaces the\n stored one whole. `get_table` marks a formula that is null when every field it reads is empty.\n\nrollup \u2014 `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`\n\n- `source_field_key` is a `select_record_link` of this table; `aggregate_option.field_key` is a\n field of the linked table, required by every operation but `count`, which counts linked records.\n- Operations by the aggregated field\'s type \u2014 none: `count`; any: `empty`, `filled`,\n `percent_empty`, `percent_filled`, `unique`, `percent_unique`; number: `sum`, `avg`, `median`,\n `min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.\n- The cell\'s type comes from the OPERATION: `earliest` and `latest` hold a date, every other\n operation a number (`date_range` counts days). A "most recent linked date" is `latest` \u2014 `min` and\n `max` are numeric and refuse a date. A rollup takes no notation of its own: `sum` over money\n reads in the aggregated field\'s currency, over a weight in its unit, a count as a plain number\n and a `percent_*` in the unit `percent`.\n- Over a figure read in each row\'s own unit or currency (a notation\'s `per_row`), `sum`,\n `avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the\n link paired with `source_field_key`, of a single select on this table \u2014 the total reads in this\n row\'s option of it. A lookup of such a field has no unit.\n- `filter` aggregates only the linked records it matches: a full group over the LINKED table\'s\n fields \u2014 `{ node_type: "group", logic, children: [{ node_type: "condition", type, field_key,\n operator, value }] }`, a select condition using `has_any_of` with an ARRAY value.\n\nlookup \u2014 `source_field_key`, `lookup_field_key`, `order_by?`\n\n- `lookup_field_key` is a field of the linked table. Without `order_by` the cell holds every linked\n record\'s value; with `order_by: { field_key, direction }` (`desc` latest, `asc` earliest) it holds\n the picked field of the single extreme record \u2014 a "latest linked X" that maintains itself.\n Ordered lookups sharing one `order_by` resolve to the SAME record.\n\n### Examples\n\n```\n{ name: "Gi\xE1 b\xE1n", type: "number", notation: { style: "currency", currency: "VND" } }\n{ name: "Tr\u1ECDng l\u01B0\u1EE3ng", type: "number", notation: { style: "unit", unit: "kg" } }\n{ name: "S\u1ED1 l\u01B0\u1EE3ng", type: "number", notation: { style: "unit", unit: { per_row: "\u0110VT" } } }\n{ name: "Chi\u1EBFt kh\u1EA5u", type: "number", notation: { style: "unit", unit: "percent" } }\n{ name: "Tr\u1EA1ng th\xE1i", type: "select", options: [{ name: "M\u1EDBi" }, { name: "Xong" }] }\n{ name: "M\xE3 \u0111\u01A1n", type: "autonumber", template: "SR-{YEAR}-{N:4}" }\n{ name: "Kh\xE1ch h\xE0ng", type: "select_record_link", table_id: "tbl_x", sync_both_ways: true }\n{ name: "Total", type: "formula", formula: { expression: "{Price} * {Qty}", notation: { style: "currency", currency: "VND" } } }\n{ name: "SL \u0111\xE3 giao", type: "rollup", source_field_key: "Giao h\xE0ng", aggregate_option: { operation: "sum", field_key: "S\u1ED1 l\u01B0\u1EE3ng" } }\n{ name: "Customer Name", type: "lookup", source_field_key: "Customer", lookup_field_key: "Name" }\n{ name: "Latest note", type: "lookup", source_field_key: "Calls", lookup_field_key: "Note", order_by: { field_key: "At", direction: "desc" } }\n```\n\n### Changing a field\n\nA field added by `update_table` shows in a view only when `add_to_views` names it, at the view\'s far\nright; on the CLI or in chat, `update_view` with `move_field` places it beside the columns it belongs with.\n\nAn `update_fields` entry is `{ field_key, name?, description?, convert_to?, \u2026 }` with the same flat\nproperties as `add_fields`, plus what only an update does:\n\n- A select\'s options change through `add_options` `[{ name, color?, mark? }]`, `update_options`\n `[{ key, name?, color?, mark? }]`, `remove_option_keys` and `reorder_options` (every existing\n option once, in order; options added in the same call follow). An option is named by its `opt_\u2026`\n key (its name also resolves); a rename keeps every record\'s value. `mark` is the option\'s brand\n (`{ kind: "brand", name: "tiktok" }`) or kit glyph (`{ kind: "icon", name: "wrench" }`), drawn in\n place of its colour dot \u2014 every option of a field has one or none does, so marking sets them all\n in one call and `mark: null` on each clears them.\n- A link\'s `sync_both_ways: false` disconnects the pair; a new `table_id` re-points it and clears its\n record data; `cardinality` is `"one"` for the child side of a parent-child pair, `"many"` for peers.\n';
|
|
44523
|
+
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, and add nothing that stores a fact the table already\nstores.** A value written as text and the same value held as a `select_record_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\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\nWorth writing as a test rather than a note, because a note about a condition nobody re-checks goes\nstale in silence.\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 \u2014 keys\nare minted per field. So anything moving a value between them needs a hand-written key map.\n\nThat map is code. Put it in one named module with a test; written inline at the copy it is invisible,\nuntested, and silently wrong the first time somebody renames an option, because a rename leaves the\nkey intact and the map still compiling. Prefer a link to a shared reference table where the set is\nopen or growing; keep a map only for a small closed set.\n\nWhere one side genuinely holds MORE values than the other, that is not drift \u2014 it is the model\ntelling the truth. The wider side must **refuse** what the narrower one cannot express rather than\nquietly picking the nearest value.\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\nDeclare the natural key in the table\'s description.\n\nAnything that imports, reconciles or de-duplicates has to decide identity, and with no declared key\nit falls back to comparing displayed text \u2014 which is how one company arrives three times under three\nspellings. Name the key: a reference number, a tax id, a link plus a period. Then match on `rec_\u2026`\nand `opt_\u2026`, never on rendered labels.\n\n## A state\'s HISTORY is rows, not columns\n\nA `changed at` column says only how long a row has been where it is now \u2014 the next move overwrites\nit \u2014 and a date column per state holds until something re-enters a state it already left.\n\nMeasuring time-in-state or conversion needs one ROW per move: a link to the subject, the state left,\nthe state entered, when. Hold those states as the source field\'s own `opt_` keys so the log carries\nno second vocabulary, and write the row in the workflow that makes the move.\n\n## Keep derived chains shallow\n\nFormulas and rollups are computed and STORED when a row is written, and one that reads another\nrecomputes with it. A rollup over a formula over a formula is paid three times on every touch, and\nagain for every row upstream of it.\n\n**Depth costs more than row count.** This is the optimisation lever that actually exists here; row\nscanning is the platform\'s problem, chain depth is yours.\n\n## Changing a money formula on a live table\n\nFormula edits recompute every row, so the only honest proof that one changed nothing it should not is\nthe numbers themselves:\n\n1. Snapshot the affected totals to a file.\n2. Make the change.\n3. Diff. Identical is the pass.\n\nAnd when a formula gains a new field, **test that the value IS the one you want, never that it\ndiffers from it** \u2014 an empty cell reads as `""`, which differs from every option key, so the inverted\nspelling silently zeroes every row written before the field existed.\n\n## Fields\n\nWhat `create_table` (on the CLI or in chat) and `update_table` take in `add_fields`, and `update_table` in `update_fields`.\n\n### Types and formats\n\n`type` is one of `text`, `number`, `date`, `boolean`, `select`, `select_member`,\n`select_record_link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`. `button` is retired: an\nexisting button field keeps running, but none is created, converted to or edited.\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### Properties\n\nA type\'s properties sit **directly on the field object** \u2014 there is no `config` wrapper. A property\nmay itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`); its inner keys stay\ninside it. Each type takes only its own:\n\n- **text** \u2014 `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).\n- **number** \u2014 `notation?`, `default_value?` (a number). `notation` is how the figure reads, one\n of three; on `update_fields` a stated one replaces the field\'s whole.\n - `{ style: "decimal" }` \u2014 a plain number, as a field stating none reads.\n - `{ style: "currency", currency: "VND" }` \u2014 money in an ISO 4217 code.\n - `{ style: "unit", unit }` \u2014 a quantity: `"percent"` (10 reads "10%", never multiplied), a\n measured code \u2014 g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day \u2014 or a counted noun such\n as `ki\u1EC7n`. A change between two units of one dimension converts every stored figure; any other\n change relabels.\n - A currency or unit read on each row is `{ per_row: "\u0110VT" }` in its place: a single select on\n the same row, or a lookup of one through a one-link, named by name or key, whose chosen option\n is that row\'s currency or unit \u2014 every option label an ISO 4217 code, or a unit as `unit`\n takes one.\n- **date** \u2014 `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,\n `derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row\'s\n creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no\n `default_value`, and holds only the `date` and `datetime` formats.\n- **boolean** \u2014 `default_value?` (`true` | `false`).\n- **select** \u2014 `options` `[{ name, color?, mark? }]`, `multi?`, `default_value?`.\n- **select_member** \u2014 `multi?`, `default_value?` (member ids).\n- **select_record_link** \u2014 `table_id`, `display_field_keys?`, `sync_both_ways?`, `cardinality?`,\n `paired_field_name?`, `paired_field_display_field_keys?`. Without `display_field_keys` a link shows\n the target\'s autonumber, else its first text field. `sync_both_ways: true` on an existing one-way\n link makes it two-way \u2014 never add a second link for that. `cardinality: "one"` marks the child\n side of a parent-child pair (the linked record is this one\'s parent); two paired sides cannot both\n be `"one"`. The `paired_*` keys name and label the back-reference the target gets.\n- **files** \u2014 no properties.\n- **autonumber** \u2014 `template` (`"INV-{YEAR}-{N:4}"`; tokens `{N}`, `{N:W}` zero-padded to W,\n `{YEAR}`, `{YEAR:2}`, `{MONTH}`, `{DAY}`), or `prefix` + `padding` (1\u201320) for `PREFIX-0001`.\n- **any type**, at create \u2014 `confirm_before_update?`: a person confirms each later edit.\n\n`default_value` fills the field on a NEW record that states no value; existing records are never\nbackfilled, and null clears it. A select\'s or a member field\'s default is an ARRAY even when the\nfield holds one (`["Unread"]`, never `"Unread"`) \u2014 option names in `add_fields`, where the keys do\nnot exist yet, and option keys (`opt_\u2026`) in `update_fields`.\n\n`default_from` (text, number, date, boolean, select, select_member, select_record_link) copies a\nvalue onto a NEW record off the row one of its links names: `{ link_field_key, field_key,\noptions? }`, the link of cardinality `"one"` and not itself copied, the source a field of the linked\ntable of this field\'s own type, date format and notation (a link one to the same table) \u2014 a formula,\na rollup, or a lookup through a one-row link (or an ordered one) read as the value it holds; several\nvalues only into a field holding several; never into a date `derive_from` stamps. It is\ncopied when the record is made and never re-read, so a later change to the source leaves the record\nalone (a line\'s price as it was when the line was booked). A value the write states wins, then the\ncopy, then `default_value`. A select source\'s `options` maps its option keys onto this field\'s\n(`update_fields`, once the keys exist); an option left unmapped is not copied. null clears it. Deleting the link a\ncopy reads through, or the field it copies off, is refused even with `force` until the copy is\ncleared; the dependency report lists the copying field as `copy_link` or `copy_source` under\n`requires_manual_action`. Any other schema change that leaves a copy unable to stand \u2014 its source\nretyped, its link made to hold many \u2014 is refused, naming the copy.\n\n### Computed fields\n\nRead-only in records. `formula` is one key holding an object; a rollup\'s and a lookup\'s properties\nare separate keys on the field \u2014 `rollup: {\u2026}` and `lookup: {\u2026}` are refused as unrecognized.\n\nformula: { expression, notation?, link? }\n\n- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never\n breaks the formula. A reference to no field, or a call to something that is no helper, is refused.\n- A select reads as an ARRAY of option keys: `{Status}[0]` for a single select,\n `includes({Tags}, "opt_\u2026")` for a multi.\n- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` \u2014 a model\n names fields by alias where a table tool names them by name or key; the language is the same.\n- `notation` is how a number result reads, as a number field\'s; `link: true` says the result is a\n URL, drawn as a link that opens.\n- On `update_fields` a key left out keeps its stored value, and a stated `notation` replaces the\n stored one whole. `get_table` marks a formula that is null when every field it reads is empty.\n\nrollup \u2014 `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`\n\n- `source_field_key` is a `select_record_link` of this table; `aggregate_option.field_key` is a\n field of the linked table, required by every operation but `count`, which counts linked records.\n- Operations by the aggregated field\'s type \u2014 none: `count`; any: `empty`, `filled`,\n `percent_empty`, `percent_filled`, `unique`, `percent_unique`; number: `sum`, `avg`, `median`,\n `min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.\n- The cell\'s type comes from the OPERATION: `earliest` and `latest` hold a date, every other\n operation a number (`date_range` counts days). A "most recent linked date" is `latest` \u2014 `min` and\n `max` are numeric and refuse a date. A rollup takes no notation of its own: `sum` over money\n reads in the aggregated field\'s currency, over a weight in its unit, a count as a plain number\n and a `percent_*` in the unit `percent`.\n- Over a figure read in each row\'s own unit or currency (a notation\'s `per_row`), `sum`,\n `avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the\n link paired with `source_field_key`, of a single select on this table \u2014 the total reads in this\n row\'s option of it. A lookup of such a field has no unit.\n- `filter` aggregates only the linked records it matches: a full group over the LINKED table\'s\n fields \u2014 `{ node_type: "group", logic, children: [{ node_type: "condition", type, field_key,\n operator, value }] }`, a select condition using `has_any_of` with an ARRAY value.\n\nlookup \u2014 `source_field_key`, `lookup_field_key`, `order_by?`\n\n- `lookup_field_key` is a field of the linked table. Without `order_by` the cell holds every linked\n record\'s value; with `order_by: { field_key, direction }` (`desc` latest, `asc` earliest) it holds\n the picked field of the single extreme record \u2014 a "latest linked X" that maintains itself.\n Ordered lookups sharing one `order_by` resolve to the SAME record.\n\n### Examples\n\n```\n{ name: "Gi\xE1 b\xE1n", type: "number", notation: { style: "currency", currency: "VND" } }\n{ name: "Tr\u1ECDng l\u01B0\u1EE3ng", type: "number", notation: { style: "unit", unit: "kg" } }\n{ name: "S\u1ED1 l\u01B0\u1EE3ng", type: "number", notation: { style: "unit", unit: { per_row: "\u0110VT" } } }\n{ name: "Chi\u1EBFt kh\u1EA5u", type: "number", notation: { style: "unit", unit: "percent" } }\n{ name: "Tr\u1EA1ng th\xE1i", type: "select", options: [{ name: "M\u1EDBi" }, { name: "Xong" }] }\n{ name: "M\xE3 \u0111\u01A1n", type: "autonumber", template: "SR-{YEAR}-{N:4}" }\n{ name: "Kh\xE1ch h\xE0ng", type: "select_record_link", table_id: "tbl_x", sync_both_ways: true }\n{ name: "Total", type: "formula", formula: { expression: "{Price} * {Qty}", notation: { style: "currency", currency: "VND" } } }\n{ name: "SL \u0111\xE3 giao", type: "rollup", source_field_key: "Giao h\xE0ng", aggregate_option: { operation: "sum", field_key: "S\u1ED1 l\u01B0\u1EE3ng" } }\n{ name: "Customer Name", type: "lookup", source_field_key: "Customer", lookup_field_key: "Name" }\n{ name: "Latest note", type: "lookup", source_field_key: "Calls", lookup_field_key: "Note", order_by: { field_key: "At", direction: "desc" } }\n```\n\n### Changing a field\n\nA field added by `update_table` shows in a view only when `add_to_views` names it, at the view\'s far\nright; on the CLI or in chat, `update_view` with `move_field` places it beside the columns it belongs with.\n\nAn `update_fields` entry is `{ field_key, name?, description?, convert_to?, \u2026 }` with the same flat\nproperties as `add_fields`, plus what only an update does:\n\n- A select\'s options change through `add_options` `[{ name, color?, mark? }]`, `update_options`\n `[{ key, name?, color?, mark? }]`, `remove_option_keys` and `reorder_options` (every existing\n option once, in order; options added in the same call follow). An option is named by its `opt_\u2026`\n key (its name also resolves); a rename keeps every record\'s value. `mark` is the option\'s brand\n (`{ kind: "brand", name: "tiktok" }`) or kit glyph (`{ kind: "icon", name: "wrench" }`), drawn in\n place of its colour dot \u2014 every option of a field has one or none does, so marking sets them all\n in one call and `mark: null` on each clears them.\n- A link\'s `sync_both_ways: false` disconnects the pair; a new `table_id` re-points it and clears its\n record data; `cardinality` is `"one"` for the child side of a parent-child pair, `"many"` for peers.\n';
|
|
44551
44524
|
|
|
44552
44525
|
// docs/filters.md
|
|
44553
44526
|
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 `fld_\u2026` key or by its name (`"Status"`).\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 names, 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 point is one of:\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` | 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## Sort\n\nA view\'s sort is a list, applied in order: `[{ "field_key": "Due", "order": "asc" | "desc" }]`.\n';
|
|
@@ -44556,7 +44529,7 @@ var filters_default = '# Filters \u2014 choosing records by their fields\n\nOne
|
|
|
44556
44529
|
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.\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` | `["opt_\u2026"]` |\n| `select_member` | `["mbr_\u2026"]` |\n| `select_record_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`, `select_member`, `select_record_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 `"opt_\u2026"` is accepted and wrapped. A single\n `select_member` takes a bare `"mbr_\u2026"` the same way.\n- `select_record_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- Two options on a single-select, or two members on a single `select_member`, are refused.\n- `update_records`\' `add_to`, `remove_from` and `replace` take arrays of the same items: `opt_` keys\n for a select, member ids for a `select_member`, record ids for a `select_record_link`, file ids for\n `files`.\n';
|
|
44557
44530
|
|
|
44558
44531
|
// docs/workflows.md
|
|
44559
|
-
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\nThe same grammar serves an automation, a table\'s lifecycle workflow and an app\'s workflow, and the\nexpressions of a table check. Only an automation\'s source opens with a trigger declaration; a\nlifecycle workflow takes its trigger from its table and event, an app workflow from the app that calls\nit.\n\n## Triggers\n\nAn automation answers an event outside the records: a schedule, a webhook, an inbound email. A\nreaction to a record being created, updated or deleted is a table lifecycle workflow (see\n**Table lifecycle workflows**).\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\nA whole automation:\n\n```\non({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });\n\nconst open = await query_records({\n table_id: "tbl_tickets",\n filters: { node_type: "condition", field_key: "fld_status", operator: "has_any_of", value: ["opt_open"] },\n});\nawait send_email({\n to: "team@example.com",\n subject: "Weekly digest",\n body: `${size(open.records)} tickets open as of ${formatDate(now(), "yyyy-MM-dd")}.`,\n});\n```\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 a connection\'s tools too, each a workflow step (`bkav_submit_invoices`, `gmail_send_email`, \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, tools, 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: { ... }? });` |\n| validate | `validate({ checks: [{ fail_when: <expr>, field_key: "...", message: <expr> }, ...] });` |\n\nEvery step\'s inputs, the values each takes and its answer: `lotics tools <name>` prints them; the steps only a body\nruns \u2014 a connection\'s, a mail\'s \u2014 are all in `workflow_steps`. `query_integration_tools` lists the workspace\'s\nconnections and the steps each opens.\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 `fld_` key of a field on a table the body names; a field of\nthe trigger table in a lifecycle workflow; in an automation 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. Tool names, the reserved\nroots, `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: record["fld_approvers"],\n prompt: `Approve the quote for ${record["fld_name"]}?`,\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, tools, model, output });` runs a model with tools as one\nstep \u2014 summarize a record, classify, draft text.\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 `record`, prior steps, `runtime` or the loop\n item here, never inside `instructions` or `tools`, which are fixed literals and read no binding.\n- `tools` \u2014 the names of the tools it may call.\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. The agent\'s own tool calls are not readable.\n\n## Keys, not names\n\n1. **Fields and options are named by key.** A field reads as `record["fld_status"]`, an option as\n `"opt_done"`, a linked field as `linked(record["fld_customer"])[0]["fld_name"]`. `get_table` lists\n each field\'s `key` and each option\'s `key`; a display name is refused at save with the key to use.\n2. **`==` against a multi-select means "includes".** `record["fld_tags"] == "opt_urgent"` on a\n multi-select is stored as `includes(record["fld_tags"], "opt_urgent")`; either spelling works.\n3. **Text renders labels.** Inside a template or a text tool input, a select, member or link value\n renders its display text: `` `${record["fld_status"]}` `` writes "Done", not `opt_done`.\n\n## What an expression reads\n\n- `record` \u2014 the trigger record (on an update, the record as it now stands).\n- `prev_record` \u2014 the record before the change, same shape (updates and deletes only).\n- `changes` \u2014 the fields an update changed, a PARTIAL map: `changes["fld_x"]?.next_value` and\n `?.prev_value`, the `?.` required (updates only).\n- `trigger` \u2014 what a non-record trigger carries (see **Triggers**). `trigger.data`,\n `trigger.prev_data` and `trigger.changes` are the same three roots spelled long; a saved body reads\n back in the short form.\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.fld_x` and `record["fld_x"]` read a field; `[n]` reads a list\'s n-th item.\n- A path may start at an awaited call: `(await get_record({...})).data["fld_x"]`. It may not start at a\n helper\'s result \u2014 `first(found.records).id` is refused; bind `first(found.records)`, then read it.\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 or member as its key or null, a link as its ids. (The record\n tools called outside a workflow return the stored arrays instead.)\n- `linked(record["fld_link"])[n]["fld_x"]` fetches the n-th linked row and reads a field of it; bare\n `linked(record["fld_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?.["fld_link"])[0]`.\n- `changes["fld_link"]?.next_value` is the same list of ids; `linked()` over a change is refused \u2014\n read `linked(record["fld_link"])` or `linked(prev_record["fld_link"])`.\n\n## Operators and statements\n\nOperators in JavaScript precedence: `? :`, `||`, `??`, `&&`, `== !=`, `< <= > >=`, `+ -`, `* /`, prefix\n`! -`.\n\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 { fld_status, fld_name } = record;` binds each name. Renames\n (`{ fld_status: s }`), quoted keys (`{ "fld_ref": ref }`), array patterns with holes\n (`const [first, , third] = xs;`) and defaults (`{ fld_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`. `=== undefined` and `!== undefined` are refused, since they\n cannot tell a missing key from null: test `isNull(x)`, or `includes(keys(o), "k")` for whether a key\n was sent.\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:** regex, `new`, `typeof`, `instanceof`, `in`, `delete`, `void`, rest elements, computed keys,\nclasses, `throw`, `import`, `export`, and a function as a value (`const f = (x) => ...`).\n\n## Helpers\n\nCalled as `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`, `toString`, `typeOf`, `parseJson`, `toJson`\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`, `substring`, `length`, `split`, `join`, `padStart(str, length, char)`,\n `padEnd(str, length, char)`, `formatNumber(x, decimals)` (ungrouped, as `x.toFixed`),\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- **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`\n- **Other**: `formatCurrency(amount, locale, currency)`, `randomNumber(len)`,\n `randomAlphaNumeric(len)`, `sample(items)` (a random item)\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\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["fld_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: "tbl_customers",\n filters: { node_type: "condition", field_key: "fld_name", operator: "equals", value: "ACME Corp" },\n});\nconst customer = first(found.records);\n\nif (customer && includes(record["fld_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\nA table can hold more rows than one page of `query_records` answers, so the body never reads "the\ntable" to see what it lacks. It takes the sheet fifty rows at a time \u2014 what one `create_records` call\nwrites \u2014 asks the table for those rows\' codes alone, and writes the rest in one call:\n\n```\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: trigger.app_workflow.inputs.file,\n columns: ["Code", "Filed", "Amount"],\n});\n\nfor (let start = 0; start < sheet.row_count; start += 50) {\n const rows = uniqueBy(slice(sheet.rows, start, start + 50), (row) => toString(row["Code"]));\n const held = await query_records({\n table_id: "tbl_declarations",\n filters: { field_key: "fld_code", operator: "is_any_of", value: rows.map((row) => toString(row["Code"])) },\n pagination: { offset: 0, limit: 1000 },\n });\n const known = held.records.map((r) => toString(r.data["fld_code"]));\n const fresh = filter(rows, (row) => !includes(known, toString(row["Code"])));\n if (size(fresh) > 0) {\n await create_records({\n table_id: "tbl_declarations",\n records: fresh.map((row) => ({\n fld_code: toString(row["Code"]),\n fld_filed: isString(row["Filed"]) ? row["Filed"] : null,\n fld_amount: toNumber(row["Amount"]),\n })),\n });\n }\n}\n```\n\n## Table lifecycle workflows\n\nA lifecycle workflow is bound to one table and one event, and its source has no `on({...})` line:\n\n- `after_create` \u2014 a create, and a draft\'s submit (the moment it becomes a record).\n- `after_update` \u2014 every field edit, a draft\'s included.\n- `after_delete`.\n\nA lifecycle workflow runs after the write commits, with every step; its errors are logged and never\nfail the write. What refuses a write before it lands is a table check (see **Table\nchecks**). An `after_update` workflow that writes its own table saves with a `loop_potential`\nwarning: its write fires it again.\n\n| reads | on |\n|---|---|\n| `record` | every event \u2014 the new record on create, the record as it now stands on update, the deleted record on delete |\n| `prev_record` | updates and deletes \u2014 the record before |\n| `changes["fld_x"]?.next_value` / `?.prev_value` | updates \u2014 the fields that changed |\n\n### Gating with `if_source`\n\n`if_source` is one JavaScript expression, reading what the body reads, tested before the workflow\nruns. When it is falsy the workflow does not run at all \u2014 no execution, no log. Use it so a workflow\nthat cares about some events only skips the rest:\n\n- a status reaching an option: `changes["fld_status"]?.next_value == "opt_done"`\n- a field cleared: `isNull(record["fld_assignee"])`\n- a direct write, not a workflow\'s cascade:\n `includes(["member", "chat_agent", "api_client"], runtime.change_origin.type)` \u2014 name every\n direct-write origin rather than testing for one: `member` is a person in the browser, and the same\n edit over MCP, the CLI or the API arrives as `api_client`.\n\nIn `if_source`, `runtime.change_origin` is the origin of the write that fired it; in the body, it is\nthe workflow\'s own.\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\nOn the CLI or in chat, `set_table_check` creates one, or changes one when given its `table_check_id`\nand only the parts that change, and `remove_table_check` removes one. `get_table` lists a table\'s\nchecks in the source they are written in.\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_id, filter, limit }`, the filter as `query_records` takes it, `limit` from 1 to 50 (50 when left out). A filter value may read the record. |\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_key` | optional; the field the refusal is shown under |\n\nEvery expression is written in the grammar above 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.\n- `lookups.<name>` \u2014 in `fail_when` and `message`, the rows that lookup read.\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), and `now()`.\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 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. 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```\n{\n "table_id": "tbl_orders",\n "name": "One open order per reference",\n "on": ["create", "update"],\n "when": "isNull(record[\\"fld_closed_at\\"])",\n "lookups": {\n "same_ref": "{ table_id: \\"tbl_orders\\", filter: { node_type: \\"group\\", logic: \\"and\\", children: [ { field_key: \\"fld_ref\\", operator: \\"equals\\", value: record[\\"fld_ref\\"] }, { field_key: \\"fld_closed_at\\", operator: \\"is_empty\\" } ] }, limit: 2 }"\n },\n "fail_when": "some(lookups.same_ref, (r) => r.id != record.id)",\n "message": "\\"An open order already has this reference.\\"",\n "field_key": "fld_ref"\n}\n```\n';
|
|
44532
|
+
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\nThe same grammar serves an automation and an app\'s workflow, and the expressions of a table check.\nOnly an automation\'s source opens with a trigger declaration; an app workflow takes its trigger from\nthe app that calls it.\n\n## Triggers\n\nAn automation answers an event outside the records: a schedule, a webhook, an inbound email.\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\nA whole automation:\n\n```\non({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });\n\nconst open = await query_records({\n table_id: "tbl_tickets",\n filters: { node_type: "condition", field_key: "fld_status", operator: "has_any_of", value: ["opt_open"] },\n});\nawait send_email({\n to: "team@example.com",\n subject: "Weekly digest",\n body: `${size(open.records)} tickets open as of ${formatDate(now(), "yyyy-MM-dd")}.`,\n});\n```\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 a connection\'s tools too, each a workflow step (`bkav_submit_invoices`, `gmail_send_email`, \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, tools, 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: { ... }? });` |\n| validate | `validate({ checks: [{ fail_when: <expr>, field_key: "...", message: <expr> }, ...] });` |\n\nEvery step\'s inputs, the values each takes and its answer: `lotics tools <name>` prints them; the steps only a body\nruns \u2014 a connection\'s, a mail\'s \u2014 are all in `workflow_steps`. `query_integration_tools` lists the workspace\'s\nconnections and the steps each opens.\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 `fld_` key 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. Tool names, the reserved\nroots, `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, tools, model, output });` runs a model with tools as one\nstep \u2014 summarize a record, classify, draft text.\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` or `tools`, which are fixed literals and read no binding.\n- `tools` \u2014 the names of the tools it may call.\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. The agent\'s own tool calls are not readable.\n\n## Keys, not names\n\n1. **Fields and options are named by key.** A field reads as `record["fld_status"]`, an option as\n `"opt_done"`, a linked field as `linked(record["fld_customer"])[0]["fld_name"]`. `get_table` lists\n each field\'s `key` and each option\'s `key`; a display name is refused at save with the key to use.\n2. **`==` against a multi-select means "includes".** `record["fld_tags"] == "opt_urgent"` on a\n multi-select is stored as `includes(record["fld_tags"], "opt_urgent")`; either spelling works.\n3. **Text renders labels.** Inside a template or a text tool input, a select, member or link value\n renders its display text: `` `${record["fld_status"]}` `` writes "Done", not `opt_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["fld_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.fld_x` and `record["fld_x"]` read a field; `[n]` reads a list\'s n-th item.\n- A path may start at an awaited call: `(await get_record({...})).data["fld_x"]`. It may not start at a\n helper\'s result \u2014 `first(found.records).id` is refused; bind `first(found.records)`, then read it.\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 or member as its key or null, a link as its ids. (The record\n tools called outside a workflow return the stored arrays instead.)\n- `linked(record["fld_link"])[n]["fld_x"]` fetches the n-th linked row and reads a field of it; bare\n `linked(record["fld_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?.["fld_link"])[0]`.\n- `changes["fld_link"]?.next_value` is the same list of ids; `linked()` over a change is refused \u2014\n read `linked(record["fld_link"])` or `linked(prev_record["fld_link"])`.\n\n## Operators and statements\n\nOperators in JavaScript precedence: `? :`, `||`, `??`, `&&`, `== !=`, `< <= > >=`, `+ -`, `* /`, prefix\n`! -`.\n\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 { fld_status, fld_name } = record;` binds each name. Renames\n (`{ fld_status: s }`), quoted keys (`{ "fld_ref": ref }`), array patterns with holes\n (`const [first, , third] = xs;`) and defaults (`{ fld_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`. `=== undefined` and `!== undefined` are refused, since they\n cannot tell a missing key from null: test `isNull(x)`, or `includes(keys(o), "k")` for whether a key\n was sent.\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:** regex, `new`, `typeof`, `instanceof`, `in`, `delete`, `void`, rest elements, computed keys,\nclasses, `throw`, `import`, `export`, and a function as a value (`const f = (x) => ...`).\n\n## Helpers\n\nCalled as `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`, `toString`, `typeOf`, `parseJson`, `toJson`\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`, `substring`, `length`, `split`, `join`, `padStart(str, length, char)`,\n `padEnd(str, length, char)`, `formatNumber(x, decimals)` (ungrouped, as `x.toFixed`),\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- **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`\n- **Other**: `formatCurrency(amount, locale, currency)`, `randomNumber(len)`,\n `randomAlphaNumeric(len)`, `sample(items)` (a random item)\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\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["fld_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: "tbl_customers",\n filters: { node_type: "condition", field_key: "fld_name", operator: "equals", value: "ACME Corp" },\n});\nconst customer = first(found.records);\nconst order = await get_record({ table_id: "tbl_orders", record_id: trigger.app_workflow.inputs.order_id });\n\nif (customer && includes(order.data["fld_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\nA table can hold more rows than one page of `query_records` answers, so the body never reads "the\ntable" to see what it lacks. It takes the sheet fifty rows at a time \u2014 what one `create_records` call\nwrites \u2014 asks the table for those rows\' codes alone, and writes the rest in one call:\n\n```\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: trigger.app_workflow.inputs.file,\n columns: ["Code", "Filed", "Amount"],\n});\n\nfor (let start = 0; start < sheet.row_count; start += 50) {\n const rows = uniqueBy(slice(sheet.rows, start, start + 50), (row) => toString(row["Code"]));\n const held = await query_records({\n table_id: "tbl_declarations",\n filters: { field_key: "fld_code", operator: "is_any_of", value: rows.map((row) => toString(row["Code"])) },\n pagination: { offset: 0, limit: 1000 },\n });\n const known = held.records.map((r) => toString(r.data["fld_code"]));\n const fresh = filter(rows, (row) => !includes(known, toString(row["Code"])));\n if (size(fresh) > 0) {\n await create_records({\n table_id: "tbl_declarations",\n records: fresh.map((row) => ({\n fld_code: toString(row["Code"]),\n fld_filed: isString(row["Filed"]) ? row["Filed"] : null,\n fld_amount: toNumber(row["Amount"]),\n })),\n });\n }\n}\n```\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\nOn the CLI or in chat, `set_table_check` creates one, or changes one when given its `table_check_id`\nand only the parts that change, and `remove_table_check` removes one. `get_table` lists a table\'s\nchecks in the source they are written in.\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_id, filter, limit }`, the filter as `query_records` takes it, `limit` from 1 to 50 (50 when left out). A filter value may read the record. |\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_key` | optional; the field the refusal is shown under |\n\nEvery expression is written in the grammar above 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.\n- `lookups.<name>` \u2014 in `fail_when` and `message`, the rows that lookup read.\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), and `now()`.\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 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. 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```\n{\n "table_id": "tbl_orders",\n "name": "One open order per reference",\n "on": ["create", "update"],\n "when": "isNull(record[\\"fld_closed_at\\"])",\n "lookups": {\n "same_ref": "{ table_id: \\"tbl_orders\\", filter: { node_type: \\"group\\", logic: \\"and\\", children: [ { field_key: \\"fld_ref\\", operator: \\"equals\\", value: record[\\"fld_ref\\"] }, { field_key: \\"fld_closed_at\\", operator: \\"is_empty\\" } ] }, limit: 2 }"\n },\n "fail_when": "some(lookups.same_ref, (r) => r.id != record.id)",\n "message": "\\"An open order already has this reference.\\"",\n "field_key": "fld_ref"\n}\n```\n';
|
|
44560
44533
|
|
|
44561
44534
|
// docs/workflow_steps.md
|
|
44562
44535
|
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`). `lotics tools <name>` prints any tool\'s inputs and answer,\nthese steps among them.\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)\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. (workflow step, 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 (workflow step, 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 `{fld_\u2026}`, 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_create_file (workflow step)\n\n- Create an Excel file with styled column headers and auto-filter enabled.\n- Input: { filename, sheets: [{ name, columns: [{ header, width? }], rows: unknown[][] }] } Headers are bold, centered, bordered, with auto-filter on the header row.\n- Best for producing polished report-style spreadsheets.\n- Returns a new file_id \u2014 use it for subsequent operations.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `sheets` | object[] | yes | |\n| `sheets[].name` | string | yes | |\n| `sheets[].columns` | object[] | yes | |\n| `sheets[].columns[].header` | string | yes | |\n| `sheets[].columns[].width` | number | | |\n| `sheets[].rows` | any[][] | yes | 2D array of cell values |\n| `filename` | string | 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### excel_find_cells (workflow step)\n\n- Search for cells matching a text query in an Excel worksheet.\n- Input: { file_id, worksheet: string\\|number, query: "search text", match_type?: "contains"\\|"exact"\\|"regex", max_results?: number } Default match_type is "contains" (case-insensitive).\n- Returns matching cell addresses, values, and positions.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `query` | string | yes | Text to find. |\n| `match_type` | "exact" \\\\| "contains" \\\\| "regex" | | How to match: exact, contains (default), or regex |\n| `max_results` | number | | Max matches to return |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `matches` | object[] | yes | |\n| `matches[].cell` | string | yes | Cell address, e.g. B3 |\n| `matches[].value` | string | yes | Cell value as string |\n| `matches[].row` | number | yes | Row number |\n| `matches[].column` | string | yes | Column letter |\n| `total_matches` | number | yes | Total number of matches found |\n\n### excel_format_range (workflow step)\n\n- Apply formatting to a cell range (fonts, fills, borders, alignment, number formats). range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:D10".\n- Never use "B2" alone.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `format` | object | yes | |\n| `format.font_bold` | boolean | | |\n| `format.font_italic` | boolean | | |\n| `format.font_size` | number | | |\n| `format.font_color` | string | | Hex color like #FF0000 |\n| `format.font_name` | string | | |\n| `format.font_underline` | "single" \\\\| "double" | | |\n| `format.font_strike` | boolean | | |\n| `format.background_color` | string | | Hex color like #FFFF00 |\n| `format.horizontal_align` | "left" \\\\| "center" \\\\| "right" | | |\n| `format.vertical_align` | "top" \\\\| "middle" \\\\| "bottom" | | |\n| `format.wrap_text` | boolean | | |\n| `format.number_format` | string | | Excel number format code (e.g. #,##0.00, dd/mm/yyyy) |\n| `format.border_top` | object | | |\n| `format.border_top.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_top.width` | number | | |\n| `format.border_top.color` | string | | |\n| `format.border_bottom` | object | | |\n| `format.border_bottom.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_bottom.width` | number | | |\n| `format.border_bottom.color` | string | | |\n| `format.border_left` | object | | |\n| `format.border_left.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_left.width` | number | | |\n| `format.border_left.color` | string | | |\n| `format.border_right` | object | | |\n| `format.border_right.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_right.width` | number | | |\n| `format.border_right.color` | string | | |\n| `column_widths` | object | | Column letter \u2192 width mapping |\n| `row_heights` | object | | Row number \u2192 height mapping |\n| `auto_filter` | string | | Range for an auto-filter (e.g. A1:D1); one over a table turns on that table\'s own filter |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `formatted_cells` | number | yes | |\n\n### excel_get_range (workflow step)\n\n- Read cell values from a range in an Excel worksheet. range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:D10".\n- Never use "B2" alone. worksheet: use sheet name (preferred) or 0-based index.\n- Use view_file first to see available worksheets.\n- Returns data keyed by row number then column letter, e.g. { "1": { "A": "Hello" } }.\n- Maximum 1000 cells per call \u2014 use smaller ranges and paginate if needed.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `options` | object | | Options for data retrieval |\n| `options.include_formulas` | boolean | | Include cell formulas instead of calculated values |\n| `options.include_formatting` | boolean | | Include cell formatting information |\n| `options.max_cell_length` | number | | Max characters returned per cell; longer text is truncated with an ellipsis. Pass -1 to return cells in full \u2014 use when reading free-text columns like notes. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | The worksheet name |\n| `range` | string | yes | The requested range |\n| `data` | object | yes | Cell values keyed by row number then column letter |\n| `formatting` | object | | Cell formatting keyed by row then column |\n| `truncated_cells` | object[] | | Cells with truncated content |\n| `truncated_cells[].cell` | string | yes | Cell address |\n| `truncated_cells[].original_length` | number | yes | Original text length |\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### excel_update_range (workflow step)\n\n- Write cell values into a range in an Excel worksheet. range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:C3".\n- Never use "B2" alone. values is a 2D array matching the range dimensions.\n- Single cell: [["value"]], row: [["a","b","c"]], grid: [[r1...],[r2...]].\n- Strings starting with "=" are interpreted as formulas (e.g. "=SUM(A1:A10)").\n- Set options.interpret_formulas=false to write literal "=" text.\n- Modifies the document in place.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `values` | any[][] | yes | 2D array of values to write (rows x columns) |\n| `options` | object | | |\n| `options.preserve_formatting` | boolean | | Preserve existing cell formatting (only update values) |\n| `options.apply_number_format` | string | | Apply a number format to all cells in the range (e.g., \'#,##0.00\') |\n| `options.interpret_formulas` | boolean | | Set false to write \'=\' strings as literal text |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | |\n| `range` | string | yes | |\n| `updated_cells` | number | yes | Number of cells updated |\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)\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)\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) |\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) |\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) |\n| `master_tracking_number` | string | | The master_tracking_number returned by fedex_create_draft_shipment \u2014 required to finalize the label (workflow step) |\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)\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) |\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_image (workflow step)\n\n- Generate an image from a text prompt and save it to the workspace.\n- Returns a file_id usable as an image field value or attachment.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `prompt` | string | yes | Description of the image to generate. |\n| `size` | "1024x1024" \\\\| "1024x1536" \\\\| "1536x1024" | | Square, portrait, or landscape. |\n| `quality` | "low" \\\\| "medium" \\\\| "high" | | Higher quality costs more. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `url` | string | yes | |\n| `width` | number | yes | |\n| `height` | number | yes | |\n| `mime_type` | 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. (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 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 (workflow step, 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## 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` | any | yes | Raw dictionary records as returned by MISA |\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)\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 | Caller-controlled UUID identifying the source document |\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) |\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 | Caller-controlled UUID identifying the source document |\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 | Caller-controlled UUID identifying the source document |\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)\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 | Caller-controlled UUID identifying the source document |\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. (workflow step, 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 (workflow step, 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## send\n\n### send_notification (workflow step)\n\n- Send a notification to workspace members. recipients is an array of principals: { type, id }.\n- All members: [{ type: "organization", id: "{{organization_id}}" }] Who triggered: [{ type: "member", id: "{{triggered_by_member_id}}" }] A group: [{ type: "member_group", id: "<group_id>" }]\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## view\n\n### view_files (workflow step)\n\n- View one or more files in a single call.\n- Two modes: - file_ids: explicit list of file IDs (use for cherry-picked files). - source: every file attached to a record\'s file field (use when you want all photos/attachments on a record).\n- Provide exactly one.\n- Reads EVERY readable format: images, PDFs, Word (.docx and legacy .doc), Excel (.xlsx and legacy .xls), CSV, and text formats (.txt, .md, .json, .xml, .yaml, .html).\n- Batch limit: 100 files.\n- Use this whenever you are handed a file_id you cannot already see \u2014 images and PDFs arrive in your context natively, but every other format is a reference you must open with this tool.\n- Files already in your context \u2014 those the user attached to the current message, and those an earlier view_files call returned this conversation \u2014 return a text-only "already in context" reference instead of re-emitting bytes. (workflow step, chat)\n- You do not need to re-call view_files to look at the same image again; scroll back to the original tool_result. (workflow step, chat)\n- Calling with already-seen file_ids is safe but wasteful.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_ids` | string[] | | |\n| `source` | object | | |\n| `source.record_id` | string | yes | |\n| `source.field_key` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `files` | object[] | yes | |\n| `files[].file_id` | string | yes | |\n| `files[].filename` | string | yes | |\n| `files[].mime_type` | string | yes | |\n| `files[].content` | object | | |\n| `files[].already_in_context` | boolean | | |\n| `files[].error` | string | | |\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## word\n\n### word_create_document (workflow step)\n\n- Create a new Word document (.docx), optionally with initial paragraphs and headings.\n- Use {{variable_name}} syntax in text to create template placeholders for data injection via create_word_template. (workflow step, chat, MCP, CLI)\n- Input: { filename, content?: [{ type: "paragraph"\\|"heading", text, level?: 1-6 }] } filename is without the .docx extension (added automatically).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `filename` | string | yes | Name of the file (without .docx extension) |\n| `content` | object[] | | |\n| `content[].type` | "paragraph" \\\\| "heading" | yes | |\n| `content[].text` | string | yes | |\n| `content[].level` | number | | Heading level (1-6), required when type is heading |\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### word_find_text (workflow step)\n\n- Search for text across all paragraphs and table cells in a Word document.\n- Input: { file_id, query: "search text", match_type?: "contains"\\|"exact"\\|"regex", max_results?: number } Default match_type is "contains".\n- Returns matching element indices, types, and text content.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `query` | string | yes | |\n| `match_type` | "contains" \\\\| "exact" \\\\| "regex" | | |\n| `max_results` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `matches` | object[] | yes | |\n| `matches[].index` | number | yes | |\n| `matches[].type` | "paragraph" \\\\| "table" \\\\| "other" | yes | |\n| `matches[].text` | string | yes | |\n| `total` | number | yes | |\n\n### word_get_content (workflow step)\n\n- Read a Word document\'s full native content, paginated.\n- Every element returned is COMPLETE \u2014 full paragraph text and full table cells \u2014 unlike view_files, whose quick-look preview truncates long paragraphs and tables. (workflow step, chat)\n- Input: { file_id, offset?, limit? }.\n- Returns elements with their 0-based indices plus next_offset when more remains; call again with offset=next_offset to continue.\n- A page may end before `limit` to keep elements whole \u2014 follow next_offset, don\'t infer completion from the count.\n- A selection mark \u2014 a highlighted, shaded or coloured run, or a shaded table cell \u2014 surfaces in `marked_text` (the paragraph with each mark shown where it falls, which is what tells you WHICH of two identical `\u25A1` was picked), `highlighted_spans` (the marked text alone) and `marked_cells` (per table), so a form\'s chosen option reads correctly even when it is marked by colour rather than a tick.\n- Bold is OFF by default and reported only with `include_bold`, as a FACT rather than a selection: nearly every heading and label is bold, so on most documents it is noise.\n- Reach for it when a form\'s chosen option carries no glyph and no colour \u2014 some forms record the choice by bolding the chosen option\'s label, and then bold is the tick\'s only trace.\n- On a PARAGRAPH, `formatting_hint` already says the line contains bold (it cannot say which run \u2014 that is what `bold_spans` adds).\n- A TABLE carries no such hint, so on a criteria or option table pass `include_bold` rather than waiting for a signal.\n- Read the result against its siblings: a wholly bold heading or header row is structure, one bold label among plain ones is the choice.\n- Footnotes surface in `footnote_refs` (per paragraph) and `footnote_cells` (per table) with the note\'s own text, since a clause often carries its rule in the footnote rather than the sentence.\n- `orphaned_footnotes` lists notes the file carries with no anchor left in the body \u2014 those render nowhere.\n- Pictures surface in `images` on the element holding them, so a paragraph carrying only a picture is not mistaken for an empty one.\n- `headers_footers` carries header and footer text, which sits outside the body and so has no element index \u2014 a form often states which form it is there rather than in its text.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `offset` | integer | | Element index to start from (pass the prior call\'s next_offset) |\n| `limit` | integer | | Max elements to return |\n| `include_bold` | boolean | | Also report WHERE the bold runs are (bold_spans / bold_cells). Off unless asked for \u2014 nearly every heading and label is bold, so it is noise on most documents and the answer on a form that ticks by bolding. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `elements` | object[] | yes | |\n| `elements[].index` | number | yes | |\n| `elements[].type` | "paragraph" \\\\| "table" \\\\| "other" | yes | |\n| `elements[].text` | string | | |\n| `elements[].rows` | number | | |\n| `elements[].cols` | number | | |\n| `elements[].cells` | string[][] | | |\n| `elements[].heading_level` | number | | |\n| `elements[].list_type` | "bullet" \\\\| "numbered" | | |\n| `elements[].list_level` | number | | |\n| `elements[].formatting_hint` | string | | |\n| `elements[].highlighted_spans` | string[] | | |\n| `elements[].bold_spans` | string[] | | |\n| `elements[].marked_text` | string | | |\n| `elements[].images` | object[] | | |\n| `elements[].images[].name` | string | | |\n| `elements[].images[].width_cm` | number | | |\n| `elements[].images[].height_cm` | number | | |\n| `elements[].marked_cells` | object[] | | |\n| `elements[].marked_cells[].row` | number | yes | |\n| `elements[].marked_cells[].col` | number | yes | |\n| `elements[].marked_cells[].spans` | string[] | yes | |\n| `elements[].bold_cells` | object[] | | |\n| `elements[].bold_cells[].row` | number | yes | |\n| `elements[].bold_cells[].col` | number | yes | |\n| `elements[].bold_cells[].spans` | string[] | yes | |\n| `elements[].footnote_refs` | object[] | | |\n| `elements[].footnote_refs[].mark` | number | yes | The superscript number the reader sees |\n| `elements[].footnote_refs[].id` | string | yes | |\n| `elements[].footnote_refs[].offset` | number | yes | Character offset into this element\'s text where the mark sits |\n| `elements[].footnote_refs[].text` | string | | |\n| `elements[].footnote_cells` | object[] | | |\n| `elements[].footnote_cells[].row` | number | yes | |\n| `elements[].footnote_cells[].col` | number | yes | |\n| `elements[].footnote_cells[].refs` | object[] | yes | |\n| `elements[].footnote_cells[].refs[].mark` | number | yes | The superscript number the reader sees |\n| `elements[].footnote_cells[].refs[].id` | string | yes | |\n| `elements[].footnote_cells[].refs[].offset` | number | yes | Character offset into this element\'s text where the mark sits |\n| `elements[].footnote_cells[].refs[].text` | string | | |\n| `total` | number | yes | |\n| `next_offset` | number \\\\| null | yes | |\n| `orphaned_footnotes` | object[] | | Notes the file still carries with no anchor left in the body \u2014 they render nowhere |\n| `orphaned_footnotes[].id` | string | yes | |\n| `orphaned_footnotes[].text` | string | yes | |\n| `headers_footers` | object[] | | Header and footer text \u2014 outside the body, so it carries no element index |\n| `headers_footers[].kind` | "header" \\\\| "footer" | yes | |\n| `headers_footers[].text` | string | yes | |\n\n### word_get_table_data (workflow step)\n\n- Read the full contents of a table in a Word document as a 2D string array.\n- Input: { file_id, table_index: number, max_rows? } table_index is the 0-based element index from view_files (must point to a table element). (workflow step, chat)\n- Returns data[][] with row_count and col_count.\n- A cell carrying a selection mark \u2014 a highlighted or shaded run, or a shaded cell \u2014 is listed in `marked_cells` ({row, col} aligned with data, spans = the marked text; empty spans = a shaded blank), so a criteria row\'s chosen option reads correctly even when it is marked by colour rather than a tick.\n- `bold_cells` lists the bold cells the same way, but ONLY with `include_bold` \u2014 header rows and label columns are bold, so it is noise on most tables.\n- Nothing in the default output hints that a table contains bold, so pass it whenever a criteria or option table shows no glyph and no colour: some forms record the choice by bolding that row\'s label and it is the tick\'s only trace.\n- It is a fact, never a selection \u2014 a whole bold header row is structure, one bold label among plain siblings is the choice.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `table_index` | number | yes | 0-based element index of the table |\n| `max_rows` | number | | Max rows to return; all rows when omitted |\n| `include_bold` | boolean | | Also report which cells are bold (bold_cells). Off unless asked for \u2014 header rows and label columns are bold, so it is noise on most tables and the answer on one that ticks by bolding. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `data` | string[][] | yes | |\n| `row_count` | number | yes | |\n| `col_count` | number | yes | |\n| `marked_cells` | object[] | | |\n| `marked_cells[].row` | number | yes | |\n| `marked_cells[].col` | number | yes | |\n| `marked_cells[].spans` | string[] | yes | |\n| `bold_cells` | object[] | | |\n| `bold_cells[].row` | number | yes | |\n| `bold_cells[].col` | number | yes | |\n| `bold_cells[].spans` | string[] | yes | |\n\n### word_insert_conditional (workflow step)\n\n- Insert {{IF variable_name}}...{{END-IF variable_name}} command rows or paragraphs into a Word document to define a conditional section.\n- For table_rows: wraps the specified row range with command rows.\n- Requires table_index, start_row, end_row.\n- For paragraphs: wraps the specified element range with command paragraphs.\n- Requires start_index, end_index.\n- Use view_files to find element indices first. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `variable_name` | string | yes | Boolean variable name |\n| `region_type` | "table_rows" \\\\| "paragraphs" | yes | |\n| `table_index` | integer | | Index of the table element (required for table_rows) |\n| `start_row` | integer | | First row to wrap (required for table_rows) |\n| `end_row` | integer | | Last row to wrap, inclusive (required for table_rows) |\n| `start_index` | integer | | First element index to wrap (required for paragraphs) |\n| `end_index` | integer | | Last element index to wrap, inclusive (required for paragraphs) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n\n### word_insert_loop (workflow step)\n\n- Insert {{FOR loop_var IN variable_name}}...{{END-FOR loop_var}} command rows or paragraphs into a Word document to define a repeating section.\n- Inside the loop, reference each item\'s fields with a $ prefix: {{$loop_var.field}} (or {{$loop_var}} for a scalar item, {{$idx}} for the 0-based index).\n- Without the $ prefix, generation fails.\n- For table_rows: wraps the specified row range with command rows.\n- Requires table_index, start_row, end_row.\n- For paragraphs: wraps the specified element range with command paragraphs.\n- Requires start_index, end_index.\n- Use view_files to find element indices first. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `variable_name` | string | yes | Array variable name |\n| `loop_variable` | string | yes | Iterator name used inside the loop, e.g. item |\n| `region_type` | "table_rows" \\\\| "paragraphs" | yes | |\n| `table_index` | integer | | Index of the table element (required for table_rows) |\n| `start_row` | integer | | First row to wrap (required for table_rows) |\n| `end_row` | integer | | Last row to wrap, inclusive (required for table_rows) |\n| `start_index` | integer | | First element index to wrap (required for paragraphs) |\n| `end_index` | integer | | Last element index to wrap, inclusive (required for paragraphs) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n\n### word_replace_text (workflow step)\n\n- Find and replace text across all paragraphs and table cells in a Word document.\n- Replaces all occurrences unless max_replacements is set.\n- A match spanning several runs collapses them into the first run\'s formatting, so tick a checkbox by matching the box character on its own \u2014 \u2610 (U+2610) becomes \u2612 (U+2612), keeping the font that draws both.\n- Matching the phrase around the box drags it into the body font; replacing it with a letter leaves a bare letter beside the boxes still showing \u2610.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `find` | string | yes | |\n| `replace` | string | yes | |\n| `match_type` | "contains" \\\\| "exact" \\\\| "regex" | | `exact` matches a whole paragraph. None of the three is case-sensitive. |\n| `max_replacements` | number | | Max number of replacements (default: unlimited) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `replacements_made` | number | yes | |\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';
|
|
@@ -46203,7 +46176,7 @@ function printSideEffects(summary) {
|
|
|
46203
46176
|
}
|
|
46204
46177
|
}
|
|
46205
46178
|
const irreversiblePart = irreversibleTools.length > 0 ? `Could NOT auto-undo (clean up manually): ${irreversibleTools.join(", ")}.` : "Could NOT auto-undo: none.";
|
|
46206
|
-
const subPart = subWorkflows ? "
|
|
46179
|
+
const subPart = subWorkflows ? " Other workflows may have run on the records it wrote \u2014 their effects are NOT in this list." : "";
|
|
46207
46180
|
console.error(`
|
|
46208
46181
|
${irreversiblePart}${subPart}`);
|
|
46209
46182
|
}
|
|
@@ -47725,8 +47698,8 @@ function printHelp() {
|
|
|
47725
47698
|
Lotics is an AI-powered operations platform. Through this CLI you can:
|
|
47726
47699
|
- Manage tables, records, and views (structured data with typed fields)
|
|
47727
47700
|
- Generate documents from templates (Excel, Word, PDF, Email)
|
|
47728
|
-
- Build and run automations \u2014 schedules, webhooks
|
|
47729
|
-
|
|
47701
|
+
- Build and run automations \u2014 schedules, webhooks and app workflows; inspect
|
|
47702
|
+
execution and version history
|
|
47730
47703
|
- Administer the workspace \u2014 invite members, manage groups, share resources,
|
|
47731
47704
|
transfer ownership, browse connected accounts (OAuth attach is web-only)
|
|
47732
47705
|
- Create and manage apps, knowledge docs, and files
|