@lotics/cli 0.293.0 → 0.294.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/dist/src/cli.js +44 -24
- package/docs/workflow_steps.md +3694 -0
- package/docs/workflows.md +3 -2
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -34226,33 +34226,37 @@ function formulaDateHasTimeOfDay(expression, fields, result) {
|
|
|
34226
34226
|
if (!m) return false;
|
|
34227
34227
|
return m[1] !== "00" || m[2] !== "00" || (m[3] ?? "00") !== "00";
|
|
34228
34228
|
}
|
|
34229
|
-
function readsFieldsOnlyThroughEmptinessPredicates(node) {
|
|
34229
|
+
function readsFieldsOnlyThroughEmptinessPredicates(node, taken) {
|
|
34230
|
+
const reads = (child) => readsFieldsOnlyThroughEmptinessPredicates(child, taken);
|
|
34230
34231
|
if (node === void 0) return true;
|
|
34231
34232
|
if (isFieldRead(node)) return false;
|
|
34232
34233
|
switch (node.type) {
|
|
34233
34234
|
case "Invoke": {
|
|
34234
34235
|
const args = node.arguments ?? [];
|
|
34235
34236
|
const predicate = node.method === void 0 && node.receiver.type === "ID" && EMPTINESS_PREDICATES.has(node.receiver.value);
|
|
34236
|
-
return (predicate ||
|
|
34237
|
-
(arg) => predicate && isFieldRead(arg) ||
|
|
34237
|
+
return (predicate || reads(node.receiver)) && args.every(
|
|
34238
|
+
(arg) => predicate && isFieldRead(arg) || reads(arg)
|
|
34238
34239
|
);
|
|
34239
34240
|
}
|
|
34240
34241
|
case "Unary":
|
|
34241
|
-
return
|
|
34242
|
+
return reads(node.child);
|
|
34242
34243
|
case "Binary":
|
|
34243
|
-
return
|
|
34244
|
+
return reads(node.left) && reads(node.right);
|
|
34244
34245
|
case "Getter":
|
|
34245
|
-
return
|
|
34246
|
+
return reads(node.receiver);
|
|
34246
34247
|
case "Index":
|
|
34247
|
-
return
|
|
34248
|
-
case "Ternary":
|
|
34249
|
-
|
|
34248
|
+
return reads(node.receiver) && reads(node.argument);
|
|
34249
|
+
case "Ternary": {
|
|
34250
|
+
if (!reads(node.condition)) return false;
|
|
34251
|
+
const decided = node.condition === void 0 || taken === void 0 ? void 0 : taken(node.condition);
|
|
34252
|
+
return decided === true ? reads(node.trueExpr) : decided === false ? reads(node.falseExpr) : reads(node.trueExpr) && reads(node.falseExpr);
|
|
34253
|
+
}
|
|
34250
34254
|
case "List":
|
|
34251
|
-
return (node.items ?? []).every(
|
|
34255
|
+
return (node.items ?? []).every((item) => reads(item));
|
|
34252
34256
|
case "Map":
|
|
34253
|
-
return Object.values(node.entries ?? {}).every(
|
|
34257
|
+
return Object.values(node.entries ?? {}).every((entry) => reads(entry));
|
|
34254
34258
|
case "ArrowFunction":
|
|
34255
|
-
return
|
|
34259
|
+
return reads(node.body);
|
|
34256
34260
|
case "ID":
|
|
34257
34261
|
return node.value !== "data";
|
|
34258
34262
|
case "Literal":
|
|
@@ -34265,8 +34269,13 @@ function isFieldRead(node) {
|
|
|
34265
34269
|
}
|
|
34266
34270
|
function evaluateFormulaWithJS(preparedExpression, data, timezone, everyReferenceEmpty) {
|
|
34267
34271
|
try {
|
|
34268
|
-
if (everyReferenceEmpty
|
|
34269
|
-
|
|
34272
|
+
if (everyReferenceEmpty) {
|
|
34273
|
+
const helpers = createTimezoneAwareFunctions(timezone);
|
|
34274
|
+
const taken = (condition) => {
|
|
34275
|
+
const answer = condition.evaluate({ ...data, ...helpers });
|
|
34276
|
+
return typeof answer === "boolean" ? answer : void 0;
|
|
34277
|
+
};
|
|
34278
|
+
if (!readsFieldsOnlyThroughEmptinessPredicates(parseExpression(preparedExpression), taken)) return null;
|
|
34270
34279
|
}
|
|
34271
34280
|
const result = evaluateJsExpression(preparedExpression, data, void 0, timezone);
|
|
34272
34281
|
if (result === null || result === void 0) {
|
|
@@ -42404,8 +42413,8 @@ function resultSideEffects(result) {
|
|
|
42404
42413
|
}
|
|
42405
42414
|
|
|
42406
42415
|
// src/version.ts
|
|
42407
|
-
var VERSION = "0.
|
|
42408
|
-
var APP_SDK_VERSION = "0.115.
|
|
42416
|
+
var VERSION = "0.294.1";
|
|
42417
|
+
var APP_SDK_VERSION = "0.115.4";
|
|
42409
42418
|
|
|
42410
42419
|
// src/timezone.ts
|
|
42411
42420
|
function machineTimezone() {
|
|
@@ -42448,7 +42457,10 @@ var filters_default = '# Filters \u2014 choosing records by their fields\n\nOne
|
|
|
42448
42457
|
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';
|
|
42449
42458
|
|
|
42450
42459
|
// docs/workflows.md
|
|
42451
|
-
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, a connection\'s steps\nincluded; `query_integration_tools` lists the workspace\'s connections 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\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';
|
|
42460
|
+
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\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';
|
|
42461
|
+
|
|
42462
|
+
// docs/workflow_steps.md
|
|
42463
|
+
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`); every other tool\'s inputs and answer are printed by\n`lotics tools <name>`.\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_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';
|
|
42452
42464
|
|
|
42453
42465
|
// docs/app_bindings.md
|
|
42454
42466
|
var app_bindings_default = "# App bindings \u2014 the queries, workflows and agents an app calls\n\nA custom-code app reaches workspace data through three kinds of binding, each declared on the app\nunder an alias \u2014 a JS identifier (`/^[a-zA-Z_$][a-zA-Z0-9_$]*$/`):\n\n| Binding | Declared with | The app calls it with | An agent calls it with |\n|---|---|---|---|\n| Query | `set_app_queries` | `useQuery(\"<alias>\", params?)` | `run_app_query` |\n| Workflow | `set_app_workflow` | `useWorkflow(\"<alias>\")({ ...inputs })` | `run_app_workflow` |\n| Agent | `set_app_agent` | `useAgentRun(\"<alias>\")({ ...inputs })` | \u2014 |\n\nEach runs under the app's authority (`{ type: \"app\", app_id }`), the app owner's reach \u2014 never the\ncalling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next\ncall uses it. `get_app_capabilities` lists an app's aliases with their params and inputs.\n\nEach reader returns a fingerprint of what it read \u2014 `get_app_query`'s `sha`, `get_app_workflow`'s\n`body_sha`, `get_app_agent`'s `instructions_sha` \u2014 and a write built on that read passes it back:\n`set_app_queries`' `expected_shas` (alias \u2192 `sha`), `set_app_workflow`'s `expected_body_sha`,\n`set_app_agent`'s `expected_instructions_sha`, `remove_app_binding`'s `expected_sha`. The write is\nrefused, and writes nothing, when the binding changed since; omit it for an unconditional write.\n\n## Queries\n\nA query declaration is `{ ast, params?, description?, templates? }`:\n\n- `ast` \u2014 a query template (a QueryNode, below). It fixes the tables, filters and columns the alias\n reads, so a caller cannot widen it.\n- `params` \u2014 the typed value holes the caller fills, each an input declaration (see **Inputs**),\n named in the ast as `{{params.<name>}}`. A param fills a VALUE position only \u2014 a filter value, a\n `search` \u2014 never a `table_id` or a field key. Every token the ast names is declared. An optional\n param left unset drops the conditions that read it; a required param that arrives empty is\n refused where a filter reads it.\n- `description` \u2014 one line saying what the query returns, for an agent choosing between aliases.\n- `templates` \u2014 the document templates an export of its rows may fill, by name:\n `{ <name>: { template_id: \"dtl_\u2026\", rows: \"<key the template lists the rows under>\" } }`.\n\nA save checks what a deploy checks: every table is in this workspace and within the owner's reach,\nevery projected, filtered and sorted field resolves, every param token is declared, and a key a\nnode's kind does not take (a stray `sort`, `limit`, `filter`, `search`) is refused by name.\n\n`set_app_queries` takes `{ <alias>: <declaration> }`, one alias or several, and merges each: send\nonly the fields you change, `null` clears `params`, `description` or `templates` (never `ast`), and a\nnew alias needs an `ast`. Any other field is refused by name. An alias it omits is kept.\n`remove_app_binding` with `kind: \"query\"` deletes one; `get_app_query` reads one back.\n\n### The query tree\n\nEvery node has a `kind`; all but `from_table` read the node under `from` (`join`: `left`/`right`,\n`union`: `sources`).\n\n| `kind` | Keys |\n|---|---|\n| `from_table` | `table_id`, `filter?`, `search?`, `sort?`, `limit?` |\n| `project` | `columns` \u2014 the output columns |\n| `filter` | `predicate` \u2014 a filter tree over the input's columns |\n| `join` | `left`, `right`, `on: { left_column, right_column }`, `type: \"inner\" \\| \"left\"` |\n| `union` | `sources` (at least two, columns aligned by name and type) |\n| `group` | `by` (columns, or `{ bucket: { source, granularity, output } }`), `aggregates` |\n| `window` | `partition_by`, `order_by`, `frame?`, `aggregates?`, `functions?` |\n| `sort` | `by: [{ field_key, order }]` |\n| `limit` | `n`, `offset?` or `keyset?` |\n| `unpivot` | `passthrough`, `row_columns`, `rows` \u2014 one source row into several |\n| `unnest` | `source`, `output`, `display_output?`, `keep_empty?` \u2014 one row per element of a multi-value cell |\n\nFilters (`from_table.filter`, `filter.predicate`, an aggregate's `filter`) are the filter grammar \u2014 the\n`filters` reference \u2014 over the input's columns, taking `{{params.x}}` as values. `search` matches\nevery text-bearing field of the row, accent- and case-insensitive.\n\nA `project` column is a field key as a bare string (output name and type come from the field), or\n`{ output?, type?, source, writable_target?, limit? }` to rename, compute or bound one. A computed\n`source` is a field key, `{ literal }`, `{ eq: [a, b] }`, `{ neq: [a, b] }`, `{ isEmpty }`,\n`{ isNotEmpty }`, `{ coalesce: [...] }`, `{ concat: [...] }`, `{ record_id: true }`,\n`{ link: { source, field } }` (a field of the first linked record),\n`{ link_agg: { source, field, operation } }` (`sum`, `avg`, `min`, `max`, `count`, `string_agg`\nover every linked record), or `{ expression }`. A computed column states its `type`: `text`,\n`number`, `boolean`, `date`, `datetime`, `select`, `select_record_link`, `select_member`, `files`\nor `json`. `limit` (1\u201310) bounds a `files` column's entries per cell.\n\nAn aggregate column is `{ output, type, operation, input_column?, filter? }` \u2014 `string_agg` also\ntakes `separator`, `distinct` and `max_values`. A `window` function is\n`{ output, fn }` for `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, plus\n`buckets` for `ntile` and `input_column`, `offset?`, `default?` for `lag` / `lead`; functions need\na non-empty `order_by`.\n\n## Inputs\n\nA query's `params`, a workflow's `inputs` and an agent's `inputs` are one vocabulary: a map of\nname \u2192 `{ type, required?, description?, \u2026per type }`. An entry is required unless it states\n`required: false`.\n\n| `type` | Takes |\n|---|---|\n| `text`, `number`, `boolean`, `date`, `datetime`, `email` | \u2014 |\n| `record_link` | `table_id`; `multi?`; `max?` (with `multi`, the most ids one call carries) |\n| `select` | exactly one of `options: [{ label, value }]` or `field: \"fld_\u2026\"` (a select field whose CURRENT options back it); `multi?` |\n| `date_range` | `include_time?` |\n| `member` | `multi?`; `group?` (a member group the value must belong to) |\n| `file` | `multi?` \u2014 an uploaded `fil_\u2026` id, for a files field |\n| `json` | \u2014 any value |\n| `object` | `fields` \u2014 a nested map of entries |\n| `array` | `items` \u2014 one entry |\n\nNesting stops at depth 8. A `field`-form select tracks the field's options as they change; an\ninline `options` set is a fixed list.\n\n## Outputs\n\nA workflow's and an agent's `outputs` declare the structured data handed back: a map of\nname \u2192 `{ type, required?, description?, \u2026per type }`, every entry present unless it states\n`required: false`.\n\n| `type` | Takes |\n|---|---|\n| `text`, `number`, `boolean`, `date`, `datetime`, `email`, `json` | \u2014 |\n| `record_link` | `table_id`, `multi?` |\n| `select` | exactly one of `options: [{ label, value }]` or `field: \"fld_\u2026\"`; `multi?` |\n| `object` | `fields` |\n| `array` | `items` |\n\nThe returned value is checked against it, and the app reads it typed. An inline `options` set is\nalso enforced on the value returned, so the producer is told the legal keys; a `field`-form select\nkeeps the type tracking the live field.\n\n## Workflows\n\n`set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,\nname?, description? }`.\n\n- `source` is the body with no `on({...})` trigger \u2014 the app is the trigger. It reads each declared\n input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id\n with `get_record`.\n The body's steps are the `workflows` reference.\n- `inputs` is the payload the call site passes (see **Inputs**). Omitted, an existing workflow keeps\n its inputs; on a first bind it takes an untyped payload. `{}` clears them.\n- `outputs` is what `return({ data })` hands back (see **Outputs**). Usually omit it: the type is\n derived from the body's `return({ data })`, and the result echoes the bound `outputs`. Declared,\n the body does not save unless its return matches.\n- A save checks the body \u2014 parse, type-check against the declared inputs, names, lint, structure \u2014\n and answers with `[source/code] location: message` lines. `verify_only: true` makes every check\n and writes nothing. A call naming a live alias replaces its body in place, keeping its run history.\n- A call whose payload does not match `inputs` is refused before the body runs.\n\n`get_app_workflow` reads the body back; `dry_run_workflow` with `trigger_type: \"app_workflow\"`\nruns it against sample inputs before binding; `remove_app_binding` with `kind: \"workflow\"` unbinds\nit.\n\n## Agents\n\n`set_app_agent` binds a streaming tool-loop agent to an alias. A run streams its work to the app,\nreturns a typed result and keeps a run history per session. The declaration:\n\n- `instructions` \u2014 the task, every run. A new alias needs it.\n- `tool_names` \u2014 the tools it may call, from these and no other: `analyze_pdf_template`,\n `code_edit_file`, `code_exec`, `code_read_file`, `code_write_file`, `excel_create_file`,\n `excel_find_cells`, `excel_format_range`, `excel_get_range`, `excel_update_range`,\n `generate_bank_qr_code`, `generate_document`, `generate_image`, `generate_qr_code`, `get_template`,\n `grep_knowledge`, `list_banks`, `list_knowledge`, `lookup_business`, `query_templates`,\n `read_knowledge`, `run_app_query`, `run_app_workflow`, `validate_excel_template`,\n `vietcombank_convert_currency`, `view_files`, `word_create_document`, `word_find_text`,\n `word_get_content`, `word_get_table_data`, `word_insert_conditional`, `word_insert_loop`,\n `word_replace_text`. `[]` is a reasoning-only agent. A few fields of a document read well through\n the `excel_*` / `word_*` tools; a cross-check, reconciliation or transform a decision rests on\n belongs in the code tools.\n- `query_aliases` / `workflow_aliases` \u2014 the app's own queries and workflows it may call through\n `run_app_query` / `run_app_workflow`. They are its whole reach over records: no agent tool takes a\n `table_id`, so a read is bounded by the query's tables, rows and columns, and a write goes through\n the app's own workflow.\n- `knowledge_doc_ids` \u2014 knowledge docs it may read with `grep_knowledge` / `read_knowledge`\n (declare them in `tool_names`). Each must be one the app owner and you can use. Nothing is\n inlined and size does not matter. The list is the agent's whole reach \u2014 `list_knowledge` finds no\n doc outside it \u2014 so name a doc's id in `instructions` to point it there. `code_exec` computes\n across a corpus (counting, cross-referencing); reading needs only the knowledge tools.\n- `model_tier` \u2014 `haiku`, `sonnet` or `opus`. Omitted, the run follows the platform's default tier\n and moves with new models; pin one only as a tested choice, and never `haiku`, a utility tier.\n- `effort_level` \u2014 `low`, `medium`, `high`, `xhigh` or `max`, one the pinned tier supports; it\n needs `model_tier`.\n- `prefix_cache_ttl` \u2014 `5m` (the default) or `1h`. `1h` doubles the cache write price and pays only\n when runs land 5 to 60 minutes apart, as a scheduled sweep does.\n- `inputs` \u2014 the per-run payload (see **Inputs**); omitted, the payload is untyped.\n- `outputs` \u2014 the structured result (see **Outputs**), checked before the run is saved and typed\n as `run.output` in the app; omitted, the result is the final message.\n- `writes` \u2014 where the outputs land: `{ table_id, row, fields }`, `row` naming a single\n `record_link` input to `table_id` and `fields` mapping each output name to a field key of that\n table. A result that validates is written to that row under the app's authority.\n\nSend only what you change: an omitted field keeps its stored value, `null` clears an optional one.\n`get_app_agent` reads one back; `remove_app_binding` with `kind: \"agent\"` deletes it.\n";
|
|
@@ -42588,7 +42600,10 @@ var JOBS = [
|
|
|
42588
42600
|
},
|
|
42589
42601
|
{
|
|
42590
42602
|
job: "Automate",
|
|
42591
|
-
sources: [
|
|
42603
|
+
sources: [
|
|
42604
|
+
{ area: "workflows", file: "docs/workflows.md", covers: "the grammar every workflow is written in: triggers, steps, expressions", readers: ["cli", "tool"] },
|
|
42605
|
+
{ area: "workflow_steps", file: "docs/workflow_steps.md", covers: "each step only a workflow body runs \u2014 a connection's, a mail's: its inputs, answer and failures", readers: ["cli", "tool"] }
|
|
42606
|
+
]
|
|
42592
42607
|
},
|
|
42593
42608
|
{
|
|
42594
42609
|
job: "Work with data",
|
|
@@ -42675,12 +42690,18 @@ function docRows(text) {
|
|
|
42675
42690
|
});
|
|
42676
42691
|
return found;
|
|
42677
42692
|
}
|
|
42693
|
+
var PARSED = /* @__PURE__ */ new Map();
|
|
42694
|
+
function parsedDoc(text) {
|
|
42695
|
+
const held = PARSED.get(text);
|
|
42696
|
+
if (held !== void 0) return held;
|
|
42697
|
+
const parsed = { lines: text.split("\n"), sections: docSections(text), rows: docRows(text) };
|
|
42698
|
+
PARSED.set(text, parsed);
|
|
42699
|
+
return parsed;
|
|
42700
|
+
}
|
|
42678
42701
|
var oneNewline = (s) => `${s.replace(/\n+$/, "")}
|
|
42679
42702
|
`;
|
|
42680
42703
|
function renderDocPage(text, args) {
|
|
42681
|
-
const lines = text
|
|
42682
|
-
const sections = docSections(text);
|
|
42683
|
-
const rows = docRows(text);
|
|
42704
|
+
const { lines, sections, rows } = parsedDoc(text);
|
|
42684
42705
|
const place = locateDocPlace(sections, rows, args.section);
|
|
42685
42706
|
if (place.kind === "miss") return { error: missError(lines, sections, rows, place, { ...args, budget: NAV_BYTES }) };
|
|
42686
42707
|
if (place.kind === "row") return { text: oneNewline([...tableHeader(lines, place.row.line), lines[place.row.line]].join("\n")) };
|
|
@@ -42722,9 +42743,7 @@ function readDocs(areas, path17, surface) {
|
|
|
42722
42743
|
function docScope(areas, path17, surface) {
|
|
42723
42744
|
const asked = resolveDocPath(areas, path17, surface);
|
|
42724
42745
|
if ("error" in asked) return asked;
|
|
42725
|
-
const lines = asked.area.text
|
|
42726
|
-
const sections = docSections(asked.area.text);
|
|
42727
|
-
const rows = docRows(asked.area.text);
|
|
42746
|
+
const { lines, sections, rows } = parsedDoc(asked.area.text);
|
|
42728
42747
|
const place = locateDocPlace(sections, rows, asked.section);
|
|
42729
42748
|
if (place.kind === "miss") {
|
|
42730
42749
|
return { error: missError(lines, sections, rows, place, { area: asked.area.area, link: surface.link, budget: NAV_BYTES }), kind: "no_part", area: asked.area.area };
|
|
@@ -42799,6 +42818,7 @@ var TEXTS = {
|
|
|
42799
42818
|
"docs/filters.md": filters_default,
|
|
42800
42819
|
"docs/field_values.md": field_values_default,
|
|
42801
42820
|
"docs/workflows.md": workflows_default,
|
|
42821
|
+
"docs/workflow_steps.md": workflow_steps_default,
|
|
42802
42822
|
"docs/app_bindings.md": app_bindings_default,
|
|
42803
42823
|
"docs/document_templates.md": document_templates_default,
|
|
42804
42824
|
"docs/knowledge_docs.md": knowledge_docs_default,
|