@lotics/cli 0.302.0 → 0.303.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli.js +316 -155
- package/docs/app_bindings.md +7 -2
- package/docs/building_an_app.md +8 -2
- package/docs/workflow_steps.md +31 -0
- package/docs/workflows.md +40 -5
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -25328,12 +25328,19 @@ var KIT_GLYPH_NAMES = [
|
|
|
25328
25328
|
|
|
25329
25329
|
// ../shared/src/icon_names.ts
|
|
25330
25330
|
var ICON_NAME_SET = new Set(ICON_NAMES);
|
|
25331
|
+
function isValidIconName(value) {
|
|
25332
|
+
return ICON_NAME_SET.has(value);
|
|
25333
|
+
}
|
|
25331
25334
|
var KIT_GLYPH_SET = new Set(KIT_GLYPH_NAMES);
|
|
25332
25335
|
function kitGlyphRefusal(name) {
|
|
25333
25336
|
if (KIT_GLYPH_SET.has(name)) return void 0;
|
|
25334
25337
|
const near = rankNames(KIT_GLYPH_NAMES, name, 5).map((one) => `"${one}"`);
|
|
25335
25338
|
return `"${name}" is not a glyph the kit draws \u2014 ${near.length === 0 ? `it draws: ${KIT_GLYPH_NAMES.join(", ")}` : `nearest: ${near.join(", ")}`}`;
|
|
25336
25339
|
}
|
|
25340
|
+
function isValidAppIcon(value) {
|
|
25341
|
+
if (value === null || value === void 0) return true;
|
|
25342
|
+
return isValidIconName(value);
|
|
25343
|
+
}
|
|
25337
25344
|
var ICON_KEYWORDS = {
|
|
25338
25345
|
shipping: ["truck", "ship", "package", "container"],
|
|
25339
25346
|
freight: ["truck", "package", "container", "ship"],
|
|
@@ -26455,9 +26462,6 @@ var workflowSchema = zod_default.object({
|
|
|
26455
26462
|
metadata: workflowMetadataSchema.nullable().optional().describe("Metadata for the workflow"),
|
|
26456
26463
|
current_version: zod_default.number().int().describe("Current version number"),
|
|
26457
26464
|
enabled: zod_default.boolean().describe("Whether this workflow is active"),
|
|
26458
|
-
imports_into: zod_default.string().nullable().optional().describe(
|
|
26459
|
-
"The table this workflow brings data into: its runs are the imports log of a register over that table that keeps one (`register.imports`). Null, it imports into none."
|
|
26460
|
-
),
|
|
26461
26465
|
workspace_id: zod_default.string().describe("ID of the workspace this workflow belongs to"),
|
|
26462
26466
|
created_at: zod_default.string().describe("Timestamp when workflow was created"),
|
|
26463
26467
|
updated_at: zod_default.string().describe("Timestamp of last update")
|
|
@@ -31809,16 +31813,6 @@ function parseWholeExpression(expression, astFactory2) {
|
|
|
31809
31813
|
function parseExpression(expression) {
|
|
31810
31814
|
return parseWholeExpression(expression, astFactory);
|
|
31811
31815
|
}
|
|
31812
|
-
function evaluateJsExpression(expression, data, additionalFunctions, defaultTimezone) {
|
|
31813
|
-
const timezoneAwareFunctions = createTimezoneAwareFunctions(defaultTimezone);
|
|
31814
|
-
const expr = parseWholeExpression(expression, astFactory);
|
|
31815
|
-
const result = expr?.evaluate({
|
|
31816
|
-
...data,
|
|
31817
|
-
...timezoneAwareFunctions,
|
|
31818
|
-
...additionalFunctions
|
|
31819
|
-
});
|
|
31820
|
-
return result;
|
|
31821
|
-
}
|
|
31822
31816
|
var EMPTINESS_PREDICATES = /* @__PURE__ */ new Set(["isEmpty", "isNull", "isNotNull"]);
|
|
31823
31817
|
function createTimezoneAwareFunctions(defaultTimezone) {
|
|
31824
31818
|
return {
|
|
@@ -31854,6 +31848,16 @@ function createTimezoneAwareFunctions(defaultTimezone) {
|
|
|
31854
31848
|
...expression_type_exports
|
|
31855
31849
|
};
|
|
31856
31850
|
}
|
|
31851
|
+
var MAX_HELD_TIMEZONES = 512;
|
|
31852
|
+
var functionsByTimezone = /* @__PURE__ */ new Map();
|
|
31853
|
+
function timezoneAwareFunctions(defaultTimezone) {
|
|
31854
|
+
const held2 = functionsByTimezone.get(defaultTimezone);
|
|
31855
|
+
if (held2 !== void 0) return held2;
|
|
31856
|
+
if (functionsByTimezone.size >= MAX_HELD_TIMEZONES) functionsByTimezone.clear();
|
|
31857
|
+
const built = Object.freeze(createTimezoneAwareFunctions(defaultTimezone));
|
|
31858
|
+
functionsByTimezone.set(defaultTimezone, built);
|
|
31859
|
+
return built;
|
|
31860
|
+
}
|
|
31857
31861
|
var numericAlphabet = customAlphabet("0123456789");
|
|
31858
31862
|
function assertRandomLength(helper, length2) {
|
|
31859
31863
|
if (length2 > MAX_RANDOM_LENGTH) {
|
|
@@ -33434,6 +33438,74 @@ var bindingSchema = zod_default.object({
|
|
|
33434
33438
|
apps: bindingMapSchema.default({})
|
|
33435
33439
|
});
|
|
33436
33440
|
|
|
33441
|
+
// ../shared/src/schemas/app_spec_rules.ts
|
|
33442
|
+
var LANE_LOADS = 2;
|
|
33443
|
+
function pressFields(reading) {
|
|
33444
|
+
switch (reading.kind) {
|
|
33445
|
+
case "breakdown":
|
|
33446
|
+
return reading.where.some((held2) => held2.field === reading.by && held2.values === 1) ? [] : [reading.by];
|
|
33447
|
+
case "pivot":
|
|
33448
|
+
return [reading.rows, reading.columns];
|
|
33449
|
+
case "metric":
|
|
33450
|
+
return reading.where.filter((held2) => !held2.me).map((held2) => held2.field);
|
|
33451
|
+
default:
|
|
33452
|
+
return [];
|
|
33453
|
+
}
|
|
33454
|
+
}
|
|
33455
|
+
function filterField(filter2) {
|
|
33456
|
+
return typeof filter2 === "string" ? filter2 : filter2.field;
|
|
33457
|
+
}
|
|
33458
|
+
function filterTiers(filter2) {
|
|
33459
|
+
return typeof filter2 === "string" ? void 0 : filter2.tiers;
|
|
33460
|
+
}
|
|
33461
|
+
function tiersAscend(tiers) {
|
|
33462
|
+
return tiers.every((tier, index) => index === 0 || tier > (tiers[index - 1] ?? tier));
|
|
33463
|
+
}
|
|
33464
|
+
var URI_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
33465
|
+
var MAIL_ADDRESS = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
33466
|
+
function contactHref(text) {
|
|
33467
|
+
const said = text.trim();
|
|
33468
|
+
if (URI_SCHEME.test(said)) return void 0;
|
|
33469
|
+
if (MAIL_ADDRESS.test(said)) return `mailto:${said}`;
|
|
33470
|
+
if (!/^\+?[\d\s().-]+$/.test(said) || !(/^[+0(]/.test(said) || /\d\s+\d/.test(said))) return void 0;
|
|
33471
|
+
const digits = said.replace(/\D/g, "");
|
|
33472
|
+
return digits.length >= 8 && digits.length <= 15 ? `tel:${said.startsWith("+") ? "+" : ""}${digits}` : void 0;
|
|
33473
|
+
}
|
|
33474
|
+
function heldTogether(one, other) {
|
|
33475
|
+
if ("value" in one || "value" in other) return "value" in one && "value" in other && one.value === other.value;
|
|
33476
|
+
return one.options.some((option) => other.options.includes(option));
|
|
33477
|
+
}
|
|
33478
|
+
function offeredTogether(one, other) {
|
|
33479
|
+
const theirs = other.when;
|
|
33480
|
+
if (one.when === void 0 || theirs === void 0) return false;
|
|
33481
|
+
const shared = one.when.flatMap((held2) => {
|
|
33482
|
+
const match2 = theirs.find((candidate) => candidate.field === held2.field);
|
|
33483
|
+
return match2 === void 0 ? [] : [heldTogether(held2, match2)];
|
|
33484
|
+
});
|
|
33485
|
+
return shared.length > 0 && shared.every(Boolean);
|
|
33486
|
+
}
|
|
33487
|
+
function isOutcome(act, acts) {
|
|
33488
|
+
return act.to !== void 0 && acts.some((other) => other.alias !== act.alias && other.to !== void 0 && other.on === act.on && other.of === act.of && offeredTogether(act, other));
|
|
33489
|
+
}
|
|
33490
|
+
function isCorrection(act, acts) {
|
|
33491
|
+
return act.danger === true && !isOutcome(act, acts);
|
|
33492
|
+
}
|
|
33493
|
+
function checkRefuses(check2, target) {
|
|
33494
|
+
const blocks = check2.blocks;
|
|
33495
|
+
if (blocks === void 0) return false;
|
|
33496
|
+
if (blocks === "all") return true;
|
|
33497
|
+
return blocks.includes(target) || target === "delete" && blocks.includes("edit");
|
|
33498
|
+
}
|
|
33499
|
+
var FUNNEL_STEPS = { min: 2, max: 8 };
|
|
33500
|
+
var FIGURE_TILES = { min: 2, max: 8 };
|
|
33501
|
+
var STATEMENT_OPS = ["plus", "minus", "times", "equals"];
|
|
33502
|
+
var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
33503
|
+
var SCAN_READS = ["text", "iso6346"];
|
|
33504
|
+
var REGISTER_PERIODS = ["today", "week", "month", "quarter", "year"];
|
|
33505
|
+
var REGISTER_PERIOD_DAYS_MAX = 366;
|
|
33506
|
+
var DASHBOARD_PERIODS = ["month", "quarter", "year"];
|
|
33507
|
+
var DASHBOARD_PERIOD_DAYS = 30;
|
|
33508
|
+
|
|
33437
33509
|
// ../shared/src/chat_models.ts
|
|
33438
33510
|
var MODEL_TIERS = ["haiku", "sonnet", "opus"];
|
|
33439
33511
|
var TIER_MODELS = {
|
|
@@ -33611,7 +33683,7 @@ var queryFromTableNodeSchema = zod_default.object({
|
|
|
33611
33683
|
table_id: zod_default.string().describe("Source table ID"),
|
|
33612
33684
|
filter: tableRecordFiltersSchema.optional(),
|
|
33613
33685
|
search: zod_default.string().optional().describe(
|
|
33614
|
-
"Free-text search across the record's
|
|
33686
|
+
"Free-text search across the record's text: every term must appear, as a substring, diacritics- and case-insensitive. It matches ALL text-bearing fields of the row, not just the query's projected columns \u2014 bound search-bearing queries accordingly. AND-ed with `filter`. Templatable with `{{params.x}}`. Prefer this over an OR-group of per-field `contains` for a search box \u2014 it can't silently miss a field and matches accented text."
|
|
33615
33687
|
),
|
|
33616
33688
|
sort: tableRecordSortSchema.optional(),
|
|
33617
33689
|
limit: zod_default.number().optional()
|
|
@@ -33646,12 +33718,20 @@ var queryGroupBucketKeySchema = zod_default.object({
|
|
|
33646
33718
|
output: zod_default.string().describe("Output column name for the bucket.")
|
|
33647
33719
|
})
|
|
33648
33720
|
});
|
|
33649
|
-
var
|
|
33721
|
+
var bandTiersSchema = zod_default.array(zod_default.number()).min(1).refine(tiersAscend, "Breakpoints ascend, each above the one before it.");
|
|
33722
|
+
var queryGroupBandKeySchema = zod_default.object({
|
|
33723
|
+
band: zod_default.object({
|
|
33724
|
+
source: zod_default.string().describe("Input number column to band."),
|
|
33725
|
+
tiers: bandTiersSchema.describe("Ascending breakpoints. A value on one is in the band above it."),
|
|
33726
|
+
output: zod_default.string().describe("Output column name for the band's key.")
|
|
33727
|
+
})
|
|
33728
|
+
});
|
|
33729
|
+
var queryGroupKeySchema = zod_default.union([zod_default.string(), queryGroupBucketKeySchema, queryGroupBandKeySchema]);
|
|
33650
33730
|
var queryGroupNodeSchema = zod_default.object({
|
|
33651
33731
|
kind: zod_default.literal("group"),
|
|
33652
33732
|
from: zod_default.lazy(() => queryNodeSchema),
|
|
33653
33733
|
by: zod_default.array(queryGroupKeySchema).describe(
|
|
33654
|
-
"Group keys: input column names,
|
|
33734
|
+
"Group keys: input column names, `{ bucket: { source, granularity, output } }` date buckets, or `{ band: { source, tiers, output } }` bands of a number. A bucket truncates a date/datetime column to the bucket START as a `date` column (wall-clock semantics; `week` starts on ISO Monday; null source \u2192 null bucket). A band answers a text column holding `from..to`, the two breakpoints the value stands between (the lower held, the upper not; an open end empty: `..10`, `10..20`, `20..`); no number \u2192 null band. Empty = single-row aggregate."
|
|
33655
33735
|
),
|
|
33656
33736
|
sets: zod_default.array(zod_default.array(zod_default.string())).min(1).optional().describe(
|
|
33657
33737
|
"Several groupings answered by one read: each lists the `by` outputs it groups by (`[]` = one row over every row). A key outside a row's set is null there, and `__grouping` holds the set's bits: one per `by` key it leaves out, the last key the lowest. Absent: one grouping by every key."
|
|
@@ -33867,6 +33947,7 @@ var appWorkflowInputSchema = zod_default.lazy(
|
|
|
33867
33947
|
appWorkflowInputArraySchema
|
|
33868
33948
|
])
|
|
33869
33949
|
);
|
|
33950
|
+
var MAX_APP_WORKFLOW_INPUT_DEPTH = 8;
|
|
33870
33951
|
var MAX_APP_WORKFLOW_OUTPUT_DEPTH = 8;
|
|
33871
33952
|
var appWorkflowOutputBaseSchema = zod_default.object({
|
|
33872
33953
|
description: zod_default.string().optional(),
|
|
@@ -34202,6 +34283,13 @@ function withLookupMetadata(lookup, info) {
|
|
|
34202
34283
|
}
|
|
34203
34284
|
|
|
34204
34285
|
// ../shared/src/formula.ts
|
|
34286
|
+
function parseOnce(evaluable) {
|
|
34287
|
+
try {
|
|
34288
|
+
return { kind: "parsed", expression: parseExpression(evaluable) };
|
|
34289
|
+
} catch (error52) {
|
|
34290
|
+
return { kind: "unparsable", error: error52 };
|
|
34291
|
+
}
|
|
34292
|
+
}
|
|
34205
34293
|
function getSyntheticValue(field) {
|
|
34206
34294
|
switch (field.type) {
|
|
34207
34295
|
case "number":
|
|
@@ -34285,7 +34373,7 @@ function inferFormulaOutputType(expression, fields) {
|
|
|
34285
34373
|
if (evaluable === null) return "text";
|
|
34286
34374
|
const shaped = shapeType(parseExpression(evaluable), new Map(fields.map((field) => [field.key, field])));
|
|
34287
34375
|
if (shaped !== void 0 && shaped !== "null") return shaped;
|
|
34288
|
-
const result = evaluateFormulaWithJS(evaluable, { data: syntheticData }, "UTC", false);
|
|
34376
|
+
const result = evaluateFormulaWithJS(parseOnce(evaluable), { data: syntheticData }, "UTC", false);
|
|
34289
34377
|
if (result === null || result === void 0) {
|
|
34290
34378
|
const sampled = sampleTypedResult(evaluable, syntheticData, expression, fields);
|
|
34291
34379
|
if (sampled !== null) return sampled;
|
|
@@ -34356,7 +34444,7 @@ function sampleTypedResult(evaluable, filled, expression, fields) {
|
|
|
34356
34444
|
assignments.push({ ...filled, [key]: null });
|
|
34357
34445
|
}
|
|
34358
34446
|
for (const data of assignments) {
|
|
34359
|
-
const result = evaluateFormulaWithJS(evaluable, { data }, "UTC", false);
|
|
34447
|
+
const result = evaluateFormulaWithJS(parseOnce(evaluable), { data }, "UTC", false);
|
|
34360
34448
|
if (result === null || result === void 0) continue;
|
|
34361
34449
|
if (typeof result === "string" && result.startsWith(FORMULA_ERROR_PREFIX)) continue;
|
|
34362
34450
|
if (typeof result === "number") return "number";
|
|
@@ -34423,17 +34511,18 @@ function readsFieldsOnlyThroughEmptinessPredicates(node, taken) {
|
|
|
34423
34511
|
function isFieldRead(node) {
|
|
34424
34512
|
return node !== void 0 && node.type === "Index" && node.receiver.type === "ID" && node.receiver.value === "data";
|
|
34425
34513
|
}
|
|
34426
|
-
function evaluateFormulaWithJS(
|
|
34514
|
+
function evaluateFormulaWithJS(parsed, data, timezone, everyReferenceEmpty) {
|
|
34427
34515
|
try {
|
|
34516
|
+
if (parsed.kind === "unparsable") throw parsed.error;
|
|
34517
|
+
const scope = { ...timezoneAwareFunctions(timezone), data: data.data };
|
|
34428
34518
|
if (everyReferenceEmpty) {
|
|
34429
|
-
const helpers = createTimezoneAwareFunctions(timezone);
|
|
34430
34519
|
const taken = (condition) => {
|
|
34431
|
-
const answer = condition.evaluate(
|
|
34520
|
+
const answer = condition.evaluate(scope);
|
|
34432
34521
|
return typeof answer === "boolean" ? answer : void 0;
|
|
34433
34522
|
};
|
|
34434
|
-
if (!readsFieldsOnlyThroughEmptinessPredicates(
|
|
34523
|
+
if (!readsFieldsOnlyThroughEmptinessPredicates(parsed.expression, taken)) return null;
|
|
34435
34524
|
}
|
|
34436
|
-
const result =
|
|
34525
|
+
const result = parsed.expression?.evaluate(scope);
|
|
34437
34526
|
if (result === null || result === void 0) {
|
|
34438
34527
|
return null;
|
|
34439
34528
|
}
|
|
@@ -34729,68 +34818,6 @@ function remapFilter(group, entity, binding, at2) {
|
|
|
34729
34818
|
return remapContractFilterGroup(group, entity, resolver);
|
|
34730
34819
|
}
|
|
34731
34820
|
|
|
34732
|
-
// ../shared/src/schemas/app_spec_rules.ts
|
|
34733
|
-
var LANE_LOADS = 2;
|
|
34734
|
-
function pressFields(reading) {
|
|
34735
|
-
switch (reading.kind) {
|
|
34736
|
-
case "breakdown":
|
|
34737
|
-
return reading.where.some((held2) => held2.field === reading.by && held2.values === 1) ? [] : [reading.by];
|
|
34738
|
-
case "pivot":
|
|
34739
|
-
return [reading.rows, reading.columns];
|
|
34740
|
-
case "metric":
|
|
34741
|
-
return reading.where.filter((held2) => !held2.me).map((held2) => held2.field);
|
|
34742
|
-
default:
|
|
34743
|
-
return [];
|
|
34744
|
-
}
|
|
34745
|
-
}
|
|
34746
|
-
function filterField(filter2) {
|
|
34747
|
-
return typeof filter2 === "string" ? filter2 : filter2.field;
|
|
34748
|
-
}
|
|
34749
|
-
var URI_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
34750
|
-
var MAIL_ADDRESS = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
34751
|
-
function contactHref(text) {
|
|
34752
|
-
const said = text.trim();
|
|
34753
|
-
if (URI_SCHEME.test(said)) return void 0;
|
|
34754
|
-
if (MAIL_ADDRESS.test(said)) return `mailto:${said}`;
|
|
34755
|
-
if (!/^\+?[\d\s().-]+$/.test(said) || !(/^[+0(]/.test(said) || /\d\s+\d/.test(said))) return void 0;
|
|
34756
|
-
const digits = said.replace(/\D/g, "");
|
|
34757
|
-
return digits.length >= 8 && digits.length <= 15 ? `tel:${said.startsWith("+") ? "+" : ""}${digits}` : void 0;
|
|
34758
|
-
}
|
|
34759
|
-
function heldTogether(one, other) {
|
|
34760
|
-
if ("value" in one || "value" in other) return "value" in one && "value" in other && one.value === other.value;
|
|
34761
|
-
return one.options.some((option) => other.options.includes(option));
|
|
34762
|
-
}
|
|
34763
|
-
function offeredTogether(one, other) {
|
|
34764
|
-
const theirs = other.when;
|
|
34765
|
-
if (one.when === void 0 || theirs === void 0) return false;
|
|
34766
|
-
const shared = one.when.flatMap((held2) => {
|
|
34767
|
-
const match2 = theirs.find((candidate) => candidate.field === held2.field);
|
|
34768
|
-
return match2 === void 0 ? [] : [heldTogether(held2, match2)];
|
|
34769
|
-
});
|
|
34770
|
-
return shared.length > 0 && shared.every(Boolean);
|
|
34771
|
-
}
|
|
34772
|
-
function isOutcome(act, acts) {
|
|
34773
|
-
return act.to !== void 0 && acts.some((other) => other.alias !== act.alias && other.to !== void 0 && other.on === act.on && other.of === act.of && offeredTogether(act, other));
|
|
34774
|
-
}
|
|
34775
|
-
function isCorrection(act, acts) {
|
|
34776
|
-
return act.danger === true && !isOutcome(act, acts);
|
|
34777
|
-
}
|
|
34778
|
-
function checkRefuses(check2, target) {
|
|
34779
|
-
const blocks = check2.blocks;
|
|
34780
|
-
if (blocks === void 0) return false;
|
|
34781
|
-
if (blocks === "all") return true;
|
|
34782
|
-
return blocks.includes(target) || target === "delete" && blocks.includes("edit");
|
|
34783
|
-
}
|
|
34784
|
-
var FUNNEL_STEPS = { min: 2, max: 8 };
|
|
34785
|
-
var FIGURE_TILES = { min: 2, max: 8 };
|
|
34786
|
-
var STATEMENT_OPS = ["plus", "minus", "times", "equals"];
|
|
34787
|
-
var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
34788
|
-
var SCAN_READS = ["text", "iso6346"];
|
|
34789
|
-
var REGISTER_PERIODS = ["today", "week", "month", "quarter", "year"];
|
|
34790
|
-
var REGISTER_PERIOD_DAYS_MAX = 366;
|
|
34791
|
-
var DASHBOARD_PERIODS = ["month", "quarter", "year"];
|
|
34792
|
-
var DASHBOARD_PERIOD_DAYS = 30;
|
|
34793
|
-
|
|
34794
34821
|
// ../shared/src/schemas/social_networks.ts
|
|
34795
34822
|
var SOCIAL_NETWORKS = ["facebook", "instagram", "threads", "x", "linkedin"];
|
|
34796
34823
|
|
|
@@ -34959,6 +34986,7 @@ var MODEL_RULES = {
|
|
|
34959
34986
|
"write.no-overlap": { sentence: "`no_overlap` spans two date fields; `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options." },
|
|
34960
34987
|
// ─── Apps ────────────────────────────────────────────────────────────────────
|
|
34961
34988
|
"app.alias-unique": { sentence: "An app's alias is unique among the model's apps, and an act's among its app's acts." },
|
|
34989
|
+
"app.icon": { sentence: "An app's `icon` is a Lucide icon name in kebab-case, as `receipt` or `trending-up` are." },
|
|
34962
34990
|
"app.records-entry": { sentence: "Every entity an app lists, opens or picks \u2014 its own, a block's, a linked row's, a document's \u2014 has a `records` entry." },
|
|
34963
34991
|
"app.reads-shared": { sentence: '`reads: "shared"` stands only where a table the app reads states a `read_scope`.' },
|
|
34964
34992
|
"app.writes-children": { sentence: 'An app that writes only the record\'s children (`writes: "children"`) adds no row of its own entity.' },
|
|
@@ -34995,6 +35023,9 @@ var MODEL_RULES = {
|
|
|
34995
35023
|
sentence: "A task entity an app lists whose rows no write of the app moves to a closed option (its ring stands disabled), or that an act ticks done and none moves back.",
|
|
34996
35024
|
note: true
|
|
34997
35025
|
},
|
|
35026
|
+
"app.workflow-value": {
|
|
35027
|
+
sentence: "A value a declared workflow (`workflows`) takes or hands back states what its type needs and nothing another type does: a `record_link` its `entity`, a `select` its `options` or the `field` holding them \u2014 never both \u2014 an `object` its `fields`, an `array` its `items`; `multi` stands on a `record_link`, `select`, `member` or `file`; an output is never a `member` or a `file`; and a value nests at most 8 deep."
|
|
35028
|
+
},
|
|
34998
35029
|
"app.declines": {
|
|
34999
35030
|
sentence: "`declines` names decided rules that fire on the app, each with why the app stays as it is: by id, and `records.picture:<entity>` and `model.template-act:<template>`, said once per entity and per template, with what they fire on."
|
|
35000
35031
|
},
|
|
@@ -35022,6 +35053,9 @@ var MODEL_RULES = {
|
|
|
35022
35053
|
sentence: "`group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and a gantt into its lanes; grouping by the title needs a subtitle to lead each row."
|
|
35023
35054
|
},
|
|
35024
35055
|
"register.readings-own": { sentence: "A register's `readings` read its own entity; another entity's stand on a dashboard." },
|
|
35056
|
+
"register.readings-bands": {
|
|
35057
|
+
sentence: "A register reading that bands a number by `tiers` has that field with the same tiers among the register's `filters`: a press on a band narrows the rows by that filter."
|
|
35058
|
+
},
|
|
35025
35059
|
"register.readings-restate": {
|
|
35026
35060
|
sentence: "A register's metric never sums the row's figure or counts the rows without a `where`: the summary line totals the figure and the status chips count the rows."
|
|
35027
35061
|
},
|
|
@@ -35053,9 +35087,12 @@ var MODEL_RULES = {
|
|
|
35053
35087
|
design: "rows-in-time"
|
|
35054
35088
|
},
|
|
35055
35089
|
// ─── Readings ────────────────────────────────────────────────────────────────
|
|
35056
|
-
"reading.value": { sentence: "A reading sums a number, never a percent \u2014 percents do not add up." },
|
|
35090
|
+
"reading.value": { sentence: "A reading sums a number, never a percent \u2014 percents do not add up; a metric's `average` reads one." },
|
|
35091
|
+
"reading.average": {
|
|
35092
|
+
sentence: "`average` averages a metric's `value` over the rows holding one, read against a plain `target` \u2014 never a value `limits` bounds, whose bound is a sum."
|
|
35093
|
+
},
|
|
35057
35094
|
"reading.split": {
|
|
35058
|
-
sentence: "`by`, `rows` and `columns` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014
|
|
35095
|
+
sentence: "`by`, `rows` and `columns` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 or by the bands `tiers` cut a number read in one unit on every row; a pivot crosses two different fields."
|
|
35059
35096
|
},
|
|
35060
35097
|
"reading.over": { sentence: "`over` is a date: the row's own, stored or a formula, or its parent's through a lookup over a one-row link." },
|
|
35061
35098
|
"reading.where": { sentence: "A reading's `where` keeps rows by a select's options, a yes/no's `true` or `false`, or a member field holding the reader (`\"me\"`)." },
|
|
@@ -35544,6 +35581,9 @@ var specFieldSchema = zod_default.object({
|
|
|
35544
35581
|
* does: nothing accumulated yet is zero, which a meter draws — any other computed value holding nothing has no
|
|
35545
35582
|
* answer. */
|
|
35546
35583
|
accumulates: zod_default.literal(true).optional(),
|
|
35584
|
+
/** Every number it holds is a whole one, by its definition: a rollup that counts, or a formula that is a count of
|
|
35585
|
+
* calendar days. A band of it cut at whole breakpoints reads to the last figure it holds. */
|
|
35586
|
+
whole: zod_default.literal(true).optional(),
|
|
35547
35587
|
/** A formula in a fixed currency or unit that computes this figure of the same row again where the row's own
|
|
35548
35588
|
* currency or unit (its notation's `per_row`) is that one — a conversion: blank on such a row. */
|
|
35549
35589
|
restates: specColumnAliasSchema.optional(),
|
|
@@ -35707,9 +35747,11 @@ var specEntitySchema = zod_default.object({
|
|
|
35707
35747
|
});
|
|
35708
35748
|
var specAggregateSchema = zod_default.object({
|
|
35709
35749
|
by: zod_default.array(specColumnAliasSchema).min(1).max(AGGREGATE_MAX_BY).optional(),
|
|
35750
|
+
bands: zod_default.record(specColumnAliasSchema, zod_default.array(zod_default.number()).min(1)).optional(),
|
|
35710
35751
|
day: specColumnAliasSchema.optional(),
|
|
35711
35752
|
sum: specColumnAliasSchema.optional()
|
|
35712
35753
|
}).strict();
|
|
35754
|
+
var specFilterSchema = zod_default.union([specColumnAliasSchema, zod_default.object({ field: specColumnAliasSchema, tiers: zod_default.array(zod_default.number()).min(1) }).strict()]);
|
|
35713
35755
|
var readingBase = {
|
|
35714
35756
|
/** The entity whose rows it reads — a key of `entities`. */
|
|
35715
35757
|
entity: contractAliasSchema,
|
|
@@ -35731,6 +35773,9 @@ var specMetricReadingSchema = zod_default.object({
|
|
|
35731
35773
|
kind: zod_default.literal("metric"),
|
|
35732
35774
|
...readingBase,
|
|
35733
35775
|
value: specColumnAliasSchema.optional(),
|
|
35776
|
+
/** Its value is averaged: its `read` keeps only the rows holding one, so a group's `__sum` over its `__count` is
|
|
35777
|
+
* their average. */
|
|
35778
|
+
average: zod_default.literal(true).optional(),
|
|
35734
35779
|
/** The date its recent periods are read by — its read's rows are per day. */
|
|
35735
35780
|
over: specColumnAliasSchema.optional(),
|
|
35736
35781
|
target: zod_default.number().positive().optional(),
|
|
@@ -35744,8 +35789,8 @@ var specMetricReadingSchema = zod_default.object({
|
|
|
35744
35789
|
var specBreakdownReadingSchema = zod_default.object({
|
|
35745
35790
|
kind: zod_default.literal("breakdown"),
|
|
35746
35791
|
...readingBase,
|
|
35747
|
-
/**
|
|
35748
|
-
by:
|
|
35792
|
+
/** What each row is counted under: the column the read is keyed by, or a number by its bands, which its read answers by each band's key. */
|
|
35793
|
+
by: specFilterSchema,
|
|
35749
35794
|
value: specColumnAliasSchema.optional(),
|
|
35750
35795
|
over: specColumnAliasSchema.optional()
|
|
35751
35796
|
}).strict();
|
|
@@ -35760,8 +35805,8 @@ var specTrendReadingSchema = zod_default.object({
|
|
|
35760
35805
|
var specPivotReadingSchema = zod_default.object({
|
|
35761
35806
|
kind: zod_default.literal("pivot"),
|
|
35762
35807
|
...readingBase,
|
|
35763
|
-
rows:
|
|
35764
|
-
columns:
|
|
35808
|
+
rows: specFilterSchema,
|
|
35809
|
+
columns: specFilterSchema,
|
|
35765
35810
|
value: specColumnAliasSchema.optional(),
|
|
35766
35811
|
over: specColumnAliasSchema.optional()
|
|
35767
35812
|
}).strict();
|
|
@@ -35849,7 +35894,6 @@ var specGanttSchema = zod_default.object({
|
|
|
35849
35894
|
/** A link column to rows of the register's own entity each row waits on, finish-to-start: an arrow from each in view. */
|
|
35850
35895
|
after: specColumnAliasSchema.optional()
|
|
35851
35896
|
}).strict();
|
|
35852
|
-
var specFilterSchema = zod_default.union([specColumnAliasSchema, zod_default.object({ field: specColumnAliasSchema, tiers: zod_default.array(zod_default.number()).min(1) }).strict()]);
|
|
35853
35897
|
var registerPeriodDaysSchema = zod_default.object({ days: zod_default.number().int().min(1).max(REGISTER_PERIOD_DAYS_MAX).describe(`How many days through today, at most ${REGISTER_PERIOD_DAYS_MAX}`) }).strict();
|
|
35854
35898
|
var registerPeriodSchema = zod_default.union([zod_default.enum(REGISTER_PERIODS), registerPeriodDaysSchema]);
|
|
35855
35899
|
var dashboardPeriodDaysSchema = zod_default.object({ days: zod_default.literal(DASHBOARD_PERIOD_DAYS).describe(`The last ${DASHBOARD_PERIOD_DAYS} days through today \u2014 the one span the switch holds`) }).strict();
|
|
@@ -35925,9 +35969,7 @@ var specRegisterSchema = zod_default.object({
|
|
|
35925
35969
|
/** Readings over the rows in view, drawn above them. */
|
|
35926
35970
|
readings: zod_default.array(specReadingSchema).optional(),
|
|
35927
35971
|
/** False: the rows keep the register's own order, none needing attention lifted first. */
|
|
35928
|
-
attention: zod_default.literal(false).optional()
|
|
35929
|
-
/** True: the register keeps a log of the runs of the workflows importing into its entity's table (`imports_into`). */
|
|
35930
|
-
imports: zod_default.literal(true).optional()
|
|
35972
|
+
attention: zod_default.literal(false).optional()
|
|
35931
35973
|
}).strict().check((ctx) => {
|
|
35932
35974
|
if (ctx.value.layout === "roster" !== (ctx.value.roster !== void 0)) {
|
|
35933
35975
|
ctx.issues.push({ code: "custom", input: ctx.value.roster, path: ["roster"], message: "a `roster` layout and its `roster` cells come together" });
|
|
@@ -36501,6 +36543,20 @@ var recordDisplaySchema = zod_default.object({
|
|
|
36501
36543
|
)
|
|
36502
36544
|
}).strict();
|
|
36503
36545
|
var recordsSchema = zod_default.record(alias, recordDisplaySchema).describe("How a row of each entity is recognised, keyed by entity alias. Every entity an app lists, opens or picks has one");
|
|
36546
|
+
var tieredFilterSchema = zod_default.object({
|
|
36547
|
+
field: fieldAlias2.describe("A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row"),
|
|
36548
|
+
tiers: zod_default.array(zod_default.number()).min(1).describe(
|
|
36549
|
+
"The breakpoints between its bands, ascending, in the field's stored unit: under the first, from each to the next, from the last up \u2014 each band worded in the field's notation; a filter picks several at once"
|
|
36550
|
+
)
|
|
36551
|
+
}).strict().check((ctx) => {
|
|
36552
|
+
const { tiers } = ctx.value;
|
|
36553
|
+
if (!tiersAscend(tiers)) {
|
|
36554
|
+
ctx.issues.push({ code: "custom", input: ctx.value, path: ["tiers"], message: "tiers ascend, each above the one before it" });
|
|
36555
|
+
}
|
|
36556
|
+
});
|
|
36557
|
+
var readingSplit = (where) => zod_default.union([fieldAlias2, tieredFilterSchema]).describe(
|
|
36558
|
+
`The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) ${where} \u2014 or \`{ "field", "tiers" }\`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart`
|
|
36559
|
+
);
|
|
36504
36560
|
var readingWhere = narrowingHolds(
|
|
36505
36561
|
'Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed'
|
|
36506
36562
|
);
|
|
@@ -36510,10 +36566,16 @@ var readingOver = fieldAlias2.describe(
|
|
|
36510
36566
|
);
|
|
36511
36567
|
var readingLabel = zod_default.string().min(1).describe("What the reading is, in the reader's words");
|
|
36512
36568
|
var optionalLabel = (fallback) => zod_default.string().min(1).optional().describe(`What the reading is, in the reader's words; absent, ${fallback}`);
|
|
36569
|
+
var sumsOrCounts = {
|
|
36570
|
+
error: (issue2) => issue2.code === "unrecognized_keys" && (issue2.keys ?? []).includes("average") ? "`average` is a metric's alone \u2014 a breakdown, a trend and a pivot count their rows or sum `value`: state the average as a `metric`" : void 0
|
|
36571
|
+
};
|
|
36513
36572
|
var readingTab = alias.describe("On a register's band, the tab whose rows it reads, whichever tab is open; absent, the open tab's");
|
|
36514
36573
|
var metricReadingSchema = zod_default.object({
|
|
36515
36574
|
metric: alias.describe("The entity whose rows are counted, or summed, into one number"),
|
|
36516
36575
|
value: readingValue.optional(),
|
|
36576
|
+
average: zod_default.literal(true).optional().describe(
|
|
36577
|
+
"The `value` is averaged, not summed: its sum over the rows holding one, divided by how many hold one \u2014 a row holding none is no part of it. Under a period each period reads its own average, and its change compares two averages; where no row holds one it reads no figure, never 0, and no change. A share is an average too: a number formula that is 100 on the rows that count and 0 on the others, in percent notation, reads as the share of rows that count"
|
|
36578
|
+
),
|
|
36517
36579
|
where: readingWhere.optional(),
|
|
36518
36580
|
tab: readingTab.optional(),
|
|
36519
36581
|
over: readingOver.optional(),
|
|
@@ -36523,13 +36585,13 @@ var metricReadingSchema = zod_default.object({
|
|
|
36523
36585
|
}).strict();
|
|
36524
36586
|
var breakdownReadingSchema = zod_default.object({
|
|
36525
36587
|
breakdown: alias.describe("The entity whose rows are split into parts"),
|
|
36526
|
-
by:
|
|
36588
|
+
by: readingSplit("each row is counted under"),
|
|
36527
36589
|
value: readingValue.optional(),
|
|
36528
36590
|
where: readingWhere.optional(),
|
|
36529
36591
|
tab: readingTab.optional(),
|
|
36530
36592
|
over: readingOver.optional(),
|
|
36531
36593
|
label: optionalLabel("the `by` field's label")
|
|
36532
|
-
}).strict();
|
|
36594
|
+
}, sumsOrCounts).strict();
|
|
36533
36595
|
var trendReadingSchema = zod_default.object({
|
|
36534
36596
|
trend: alias.describe("The entity whose rows are counted, or summed, per period"),
|
|
36535
36597
|
over: fieldAlias2.describe("The date each row falls on \u2014 its own (stored, or a formula), or its parent's through a lookup over a one-link"),
|
|
@@ -36538,17 +36600,17 @@ var trendReadingSchema = zod_default.object({
|
|
|
36538
36600
|
where: readingWhere.optional(),
|
|
36539
36601
|
tab: readingTab.optional(),
|
|
36540
36602
|
label: optionalLabel("the value's label, else the entity's")
|
|
36541
|
-
}).strict();
|
|
36603
|
+
}, sumsOrCounts).strict();
|
|
36542
36604
|
var pivotReadingSchema = zod_default.object({
|
|
36543
36605
|
pivot: alias.describe("The entity whose rows are counted, or summed, in a grid"),
|
|
36544
|
-
rows:
|
|
36545
|
-
columns:
|
|
36606
|
+
rows: readingSplit("down the side"),
|
|
36607
|
+
columns: readingSplit("across the top"),
|
|
36546
36608
|
value: readingValue.optional(),
|
|
36547
36609
|
where: readingWhere.optional(),
|
|
36548
36610
|
tab: readingTab.optional(),
|
|
36549
36611
|
over: readingOver.optional(),
|
|
36550
36612
|
label: optionalLabel("the two fields' labels")
|
|
36551
|
-
}).strict();
|
|
36613
|
+
}, sumsOrCounts).strict();
|
|
36552
36614
|
var progressReadingSchema = zod_default.object({
|
|
36553
36615
|
progress: alias.describe("The entity whose rows are the parts of the work, read as the share of it planned and the share done, day by day"),
|
|
36554
36616
|
planned: zod_default.object({
|
|
@@ -36581,17 +36643,6 @@ var dashboardReadingSchema = zod_default.union([metricReadingSchema, breakdownRe
|
|
|
36581
36643
|
});
|
|
36582
36644
|
var readingSchema = zod_default.union([...figureReadingSchema.options, listReadingSchema]);
|
|
36583
36645
|
var LINE_FILTERS = 3;
|
|
36584
|
-
var tieredFilterSchema = zod_default.object({
|
|
36585
|
-
field: fieldAlias2.describe("A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row"),
|
|
36586
|
-
tiers: zod_default.array(zod_default.number()).min(1).describe(
|
|
36587
|
-
"The breakpoints between its bands, ascending, in the field's stored unit: under the first, from each to the next, from the last up \u2014 each band worded in the field's notation, several picked at once"
|
|
36588
|
-
)
|
|
36589
|
-
}).strict().check((ctx) => {
|
|
36590
|
-
const { tiers } = ctx.value;
|
|
36591
|
-
if (tiers.some((tier, index) => index > 0 && tier <= (tiers[index - 1] ?? tier))) {
|
|
36592
|
-
ctx.issues.push({ code: "custom", input: ctx.value, path: ["tiers"], message: "tiers ascend, each above the one before it" });
|
|
36593
|
-
}
|
|
36594
|
-
});
|
|
36595
36646
|
var LINE_EDITS = 2;
|
|
36596
36647
|
var lineFilters = (owner) => zod_default.array(zod_default.union([fieldAlias2, tieredFilterSchema])).max(LINE_FILTERS, { error: `${owner} states at most ${LINE_FILTERS} filters \u2014 keep the ones narrowed by daily` });
|
|
36597
36648
|
var sortKeySchema = zod_default.object({
|
|
@@ -36708,8 +36759,7 @@ var registerSchema = zod_default.object({
|
|
|
36708
36759
|
readings: zod_default.array(figureReadingSchema).min(1).optional().describe(
|
|
36709
36760
|
"Readings of the register's own entity above its rows, over the rows in view \u2014 the search, the status and the filters narrow them, but a picture's own field, read as the register opens on it. A press on a breakdown's part, a pivot's cell or a figure with `where` narrows the rows by its field, as a filter does"
|
|
36710
36761
|
),
|
|
36711
|
-
attention: zod_default.literal(false).optional().describe("false: a table's or cards' rows keep the register's own order (a first-come queue), and no column names whose step each row stands at; absent, those needing attention open first \u2014 those at a step the reader owns, then overdue, then due within 3 days, then those whose line states a warning \u2014 each part in that order, until the reader picks another")
|
|
36712
|
-
imports: zod_default.literal(true).optional().describe("true: the register keeps a log of the runs of the workflows marked as importing into its table (a workflow's `imports_into`) \u2014 each run's end and the rows it added, changed and removed of those the reader sees; absent, none")
|
|
36762
|
+
attention: zod_default.literal(false).optional().describe("false: a table's or cards' rows keep the register's own order (a first-come queue), and no column names whose step each row stands at; absent, those needing attention open first \u2014 those at a step the reader owns, then overdue, then due within 3 days, then those whose line states a warning \u2014 each part in that order, until the reader picks another")
|
|
36713
36763
|
}).strict();
|
|
36714
36764
|
var shownWhen = conditionsHold(
|
|
36715
36765
|
"Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value"
|
|
@@ -37032,6 +37082,29 @@ var checkSchema = zod_default.object({
|
|
|
37032
37082
|
blocks: zod_default.union([zod_default.literal("all"), zod_default.array(zod_default.union([alias, zod_default.literal("edit"), zod_default.literal("delete")])).min(1)]).optional().describe('Acts, "edit" (the saves and the Delete), "delete" (the Delete alone) or "all" that the standing check refuses; absent, it only warns'),
|
|
37033
37083
|
resolve: alias.optional().describe("An act of this app, on the rows the check stands on, that clears it: the check's line links to the act where it stands, and the act keeps its own conditions")
|
|
37034
37084
|
}).strict();
|
|
37085
|
+
var MODEL_WORKFLOW_VALUE_TYPES = ["text", "number", "boolean", "date", "datetime", "email", "json", "record_link", "select", "member", "file", "object", "array"];
|
|
37086
|
+
var MODEL_SELECT_REF = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
37087
|
+
var modelWorkflowValueSchema = zod_default.lazy(
|
|
37088
|
+
() => zod_default.object({
|
|
37089
|
+
type: zod_default.enum(MODEL_WORKFLOW_VALUE_TYPES).describe("What the value is"),
|
|
37090
|
+
description: zod_default.string().min(1).optional().describe("What the value is for, read by whoever calls the workflow"),
|
|
37091
|
+
required: zod_default.literal(false).optional().describe("false: the value may be left out; absent, it is always there"),
|
|
37092
|
+
multi: zod_default.literal(true).optional().describe("A `record_link`, `select`, `member` or `file` holding several"),
|
|
37093
|
+
entity: alias.optional().describe("The entity a `record_link` names a row of"),
|
|
37094
|
+
field: zod_default.string().regex(MODEL_SELECT_REF).optional().describe("`<entity>.<field>`: the select field whose options a `select` takes, as the field holds them when the workflow runs"),
|
|
37095
|
+
options: zod_default.array(alias).min(1).optional().describe("The values a `select` takes, where no field holds them"),
|
|
37096
|
+
fields: zod_default.record(zod_default.string().min(1), modelWorkflowValueSchema).optional().describe("An `object`'s values, by name"),
|
|
37097
|
+
items: modelWorkflowValueSchema.optional().describe("What each item of an `array` is")
|
|
37098
|
+
}).strict()
|
|
37099
|
+
);
|
|
37100
|
+
var modelWorkflowSchema = zod_default.object({
|
|
37101
|
+
description: zod_default.string().min(1).max(MAX_APP_CAPABILITY_DESCRIPTION).describe("What the workflow does, read by whoever calls it by its alias"),
|
|
37102
|
+
inputs: zod_default.record(zod_default.string().min(1), modelWorkflowValueSchema).optional().describe("What a call hands it, by name \u2014 checked before its body runs"),
|
|
37103
|
+
outputs: zod_default.record(zod_default.string().min(1), modelWorkflowValueSchema).optional().describe("What its body returns as `data`, by name \u2014 checked as it returns; absent, read off the body")
|
|
37104
|
+
}).strict();
|
|
37105
|
+
var appWorkflows = zod_default.record(alias, modelWorkflowSchema).optional().describe(
|
|
37106
|
+
"Workflows of the app no act runs, by alias \u2014 called through the app (`run_app_workflow`, its API) by an agent or a service: each states what it takes and hands back, and its body is set on the app (`set_app_workflow`) and kept by every apply. The screen draws nothing for one"
|
|
37107
|
+
);
|
|
37035
37108
|
var RULE_ID = /^[a-z]+\.[a-z-]+$/;
|
|
37036
37109
|
var declineKey = zod_default.string().refine((key) => {
|
|
37037
37110
|
const [rule = "", of, ...more] = key.split(":");
|
|
@@ -37041,7 +37114,9 @@ var appAlias = alias.describe("The app's name within the model \u2014 what `--fr
|
|
|
37041
37114
|
var appName = zod_default.string().min(1).describe("The job, in the words of the people who do it");
|
|
37042
37115
|
var appDescription = zod_default.string().min(1).optional().describe("What the job is for, in a sentence");
|
|
37043
37116
|
var appIcon = zod_default.string().min(1).optional().describe("A lucide icon name the launcher tile draws");
|
|
37044
|
-
var
|
|
37117
|
+
var appThemeBody = zod_default.object({ color: optionColorSchema.describe("The tile's colour") }).strict();
|
|
37118
|
+
var appTheme = appThemeBody.optional().describe("The launcher tile's colour");
|
|
37119
|
+
var statableAppRowSchema = zod_default.object({ name: appName, theme: appThemeBody.nullable() }).partial();
|
|
37045
37120
|
var appReads = zod_default.literal("shared").optional().describe("Every member reads every row, whatever the entity's read scope");
|
|
37046
37121
|
var appRowSchema = zod_default.object({
|
|
37047
37122
|
subtitle: zod_default.array(fieldAlias2).min(1).max(2).optional().describe("Up to two fields read under the title in this app, in place of the entity's"),
|
|
@@ -37076,6 +37151,7 @@ var modelAppSchema = zod_default.object({
|
|
|
37076
37151
|
record: recordSchema.optional().describe("What a row opens: the door, its sections, and the Activity its head opens \u2014 its saves, and its comments where stated"),
|
|
37077
37152
|
acts: zod_default.array(actSchema).optional().describe("What the reader does to a record \u2014 each a press with its conditions and its write"),
|
|
37078
37153
|
checks: zod_default.array(checkSchema).optional().describe("Formulas that warn while they stand, or refuse the acts and saves they name"),
|
|
37154
|
+
workflows: appWorkflows,
|
|
37079
37155
|
declines: zod_default.record(declineKey, zod_default.string().trim().min(1, "says why the app stays as it is")).optional().describe(
|
|
37080
37156
|
"Decided rules this app stays outside of, each with why in a line: by id, and `records.picture:<entity>` and `model.template-act:<template>` with what they fire on. Read by the apply's check alone, never by the app"
|
|
37081
37157
|
)
|
|
@@ -37090,7 +37166,8 @@ var dashboardAppSchema = zod_default.object({
|
|
|
37090
37166
|
dashboard: zod_default.array(dashboardReadingSchema).min(1).describe("The readings, in order \u2014 each over every row of its entity, windowed by the one period the reader switches"),
|
|
37091
37167
|
period: dashboardPeriodSchema.optional().describe(
|
|
37092
37168
|
"The period the switch opens on \u2014 `month`, `quarter` or `year` so far, or `{ days: 30 }`, the last 30 days through today; absent, this month so far. Needs a reading placed in time by `over`"
|
|
37093
|
-
)
|
|
37169
|
+
),
|
|
37170
|
+
workflows: appWorkflows
|
|
37094
37171
|
}).strict();
|
|
37095
37172
|
var modelAppsSchema = zod_default.array(zod_default.union([modelAppSchema, dashboardAppSchema])).describe("The apps this workspace will have \u2014 each one register over an entity, or a dashboard of readings");
|
|
37096
37173
|
function isDashboardApp(app) {
|
|
@@ -38403,6 +38480,7 @@ function resolveFigure(model, reading, at2) {
|
|
|
38403
38480
|
kind: "metric",
|
|
38404
38481
|
...common(reading.metric),
|
|
38405
38482
|
label: reading.label,
|
|
38483
|
+
...reading.average === void 0 ? {} : { average: reading.average },
|
|
38406
38484
|
...reading.over === void 0 ? {} : { over: reading.over },
|
|
38407
38485
|
...reading.target === void 0 ? {} : { target: reading.target },
|
|
38408
38486
|
...reading.better === void 0 ? {} : { better: reading.better },
|
|
@@ -38416,7 +38494,7 @@ function resolveFigure(model, reading, at2) {
|
|
|
38416
38494
|
reading: {
|
|
38417
38495
|
kind: "breakdown",
|
|
38418
38496
|
...common(reading.breakdown),
|
|
38419
|
-
label: settled(reading.label, () => labelOf(reading.breakdown, reading.by)),
|
|
38497
|
+
label: settled(reading.label, () => labelOf(reading.breakdown, filterField(reading.by))),
|
|
38420
38498
|
by: reading.by,
|
|
38421
38499
|
...reading.over === void 0 ? {} : { over: reading.over }
|
|
38422
38500
|
}
|
|
@@ -38441,7 +38519,7 @@ function resolveFigure(model, reading, at2) {
|
|
|
38441
38519
|
reading: {
|
|
38442
38520
|
kind: "pivot",
|
|
38443
38521
|
...common(reading.pivot),
|
|
38444
|
-
label: settled(reading.label, () => `${labelOf(reading.pivot, reading.rows)} \xD7 ${labelOf(reading.pivot, reading.columns)}`),
|
|
38522
|
+
label: settled(reading.label, () => `${labelOf(reading.pivot, filterField(reading.rows))} \xD7 ${labelOf(reading.pivot, filterField(reading.columns))}`),
|
|
38445
38523
|
rows: reading.rows,
|
|
38446
38524
|
columns: reading.columns,
|
|
38447
38525
|
...reading.over === void 0 ? {} : { over: reading.over }
|
|
@@ -38471,11 +38549,11 @@ function readingFields(reading) {
|
|
|
38471
38549
|
case "metric":
|
|
38472
38550
|
return unique2([...reading.value === void 0 ? [] : [reading.value], ...reading.bound === void 0 ? [] : [reading.bound], ...reading.over === void 0 ? [] : [reading.over], ...where]);
|
|
38473
38551
|
case "breakdown":
|
|
38474
|
-
return unique2([reading.by, ...reading.value === void 0 ? [] : [reading.value], ...reading.over === void 0 ? [] : [reading.over], ...where]);
|
|
38552
|
+
return unique2([filterField(reading.by), ...reading.value === void 0 ? [] : [reading.value], ...reading.over === void 0 ? [] : [reading.over], ...where]);
|
|
38475
38553
|
case "trend":
|
|
38476
38554
|
return unique2([reading.over, ...reading.value === void 0 ? [] : [reading.value], ...where]);
|
|
38477
38555
|
case "pivot":
|
|
38478
|
-
return unique2([reading.rows, reading.columns, ...reading.value === void 0 ? [] : [reading.value], ...reading.over === void 0 ? [] : [reading.over], ...where]);
|
|
38556
|
+
return unique2([filterField(reading.rows), filterField(reading.columns), ...reading.value === void 0 ? [] : [reading.value], ...reading.over === void 0 ? [] : [reading.over], ...where]);
|
|
38479
38557
|
case "progress":
|
|
38480
38558
|
return unique2([reading.planned.start, reading.planned.end, reading.done, ...reading.finished === void 0 ? [] : [reading.finished], ...reading.weight === void 0 ? [] : [reading.weight], ...where]);
|
|
38481
38559
|
case "list":
|
|
@@ -39131,6 +39209,48 @@ function checkChecklist(model, acts, child, block, at2) {
|
|
|
39131
39209
|
return findings;
|
|
39132
39210
|
}
|
|
39133
39211
|
|
|
39212
|
+
// ../shared/src/schemas/app_model_validate_workflows.ts
|
|
39213
|
+
var OWN_KEY = { record_link: "entity", object: "fields", array: "items" };
|
|
39214
|
+
var SEVERAL = /* @__PURE__ */ new Set(["record_link", "select", "member", "file"]);
|
|
39215
|
+
var TAKEN_ONLY = /* @__PURE__ */ new Set(["member", "file"]);
|
|
39216
|
+
function checkValue(model, value, half, at2, depth) {
|
|
39217
|
+
if (depth > MAX_APP_WORKFLOW_INPUT_DEPTH) return [error51("app.workflow-value", at2, `nests more than ${MAX_APP_WORKFLOW_INPUT_DEPTH} deep`)];
|
|
39218
|
+
const findings = [];
|
|
39219
|
+
for (const [type, key] of Object.entries(OWN_KEY)) {
|
|
39220
|
+
if (value.type === type !== (value[key] !== void 0)) {
|
|
39221
|
+
findings.push(error51("app.workflow-value", at2, value.type === type ? `is a ${type} with no \`${key}\`` : `is a ${value.type} and states \`${key}\`, which only a ${type} does`));
|
|
39222
|
+
}
|
|
39223
|
+
}
|
|
39224
|
+
const sources = [value.options, value.field].filter((source) => source !== void 0).length;
|
|
39225
|
+
if (value.type === "select" ? sources !== 1 : sources > 0) {
|
|
39226
|
+
findings.push(
|
|
39227
|
+
error51("app.workflow-value", at2, value.type === "select" ? "is a select stating its `options`, or the `field` holding them \u2014 one of the two" : `is a ${value.type} and states a select's \`options\` or \`field\``)
|
|
39228
|
+
);
|
|
39229
|
+
}
|
|
39230
|
+
if (value.multi === true && !SEVERAL.has(value.type)) findings.push(error51("app.workflow-value", `${at2}.multi`, `is a ${value.type}, which holds one`));
|
|
39231
|
+
if (half === "outputs" && TAKEN_ONLY.has(value.type)) findings.push(error51("app.workflow-value", `${at2}.type`, `hands back a ${value.type}, which a workflow only takes`));
|
|
39232
|
+
if (value.entity !== void 0 && entityOf(model, value.entity) === void 0) {
|
|
39233
|
+
findings.push(error51("model.names-declared", `${at2}.entity`, `names entity "${value.entity}", which this model does not declare`));
|
|
39234
|
+
}
|
|
39235
|
+
if (value.field !== void 0) {
|
|
39236
|
+
const [entity = "", name = ""] = value.field.split(".");
|
|
39237
|
+
const held2 = entityOf(model, entity);
|
|
39238
|
+
const field = held2 === void 0 ? void 0 : fieldOf(held2, name);
|
|
39239
|
+
if (field === void 0) findings.push(error51("model.names-declared", `${at2}.field`, `names "${value.field}", which is no field of an entity this model declares`));
|
|
39240
|
+
else if (field.type !== "select") findings.push(error51("app.workflow-value", `${at2}.field`, `names "${value.field}", a ${field.type} field \u2014 a select's options are a select field's`));
|
|
39241
|
+
}
|
|
39242
|
+
for (const [name, inner] of Object.entries(value.fields ?? {})) findings.push(...checkValue(model, inner, half, `${at2}.fields.${name}`, depth + 1));
|
|
39243
|
+
if (value.items !== void 0) findings.push(...checkValue(model, value.items, half, `${at2}.items`, depth + 1));
|
|
39244
|
+
return findings;
|
|
39245
|
+
}
|
|
39246
|
+
function checkWorkflows(model, app, at2) {
|
|
39247
|
+
return Object.entries(app.workflows ?? {}).flatMap(
|
|
39248
|
+
([alias2, workflow]) => ["inputs", "outputs"].flatMap(
|
|
39249
|
+
(half) => Object.entries(workflow[half] ?? {}).flatMap(([name, value]) => checkValue(model, value, half, `${at2}.workflows.${alias2}.${half}.${name}`, 1))
|
|
39250
|
+
)
|
|
39251
|
+
);
|
|
39252
|
+
}
|
|
39253
|
+
|
|
39134
39254
|
// ../shared/src/schemas/app_model_validate_readings.ts
|
|
39135
39255
|
function isYesNo(field, entity, entities) {
|
|
39136
39256
|
return field.type === "boolean" || field.type === "formula" && resolvedFieldType(field, entity, entities) === "boolean";
|
|
@@ -39165,6 +39285,15 @@ function notInTime(field, entity, entities) {
|
|
|
39165
39285
|
if (resolvedFieldType(field, entity, entities) === "date" && (field.type === "date" || field.type === "formula" || field.type === "lookup")) return void 0;
|
|
39166
39286
|
return `is ${kindOf(field, entity, entities)} \u2014 a reading places its rows in time by a date: the row's own, or its parent's through a lookup over a one-link`;
|
|
39167
39287
|
}
|
|
39288
|
+
function bandsRefusal(model, entity, field) {
|
|
39289
|
+
if (resolvedFieldType(field, entity, model.entities) !== "number") return `"${field.alias}" is ${kindOf(field, entity, model.entities)} \u2014 tiers band a number; name it bare`;
|
|
39290
|
+
const select = contractFigureSelect(field, entity, model.entities);
|
|
39291
|
+
return select === void 0 ? void 0 : `"${field.alias}" is read in each row's own ${select.vocabulary} ("${select.alias}"), so a breakpoint is a different amount on each row \u2014 name it bare`;
|
|
39292
|
+
}
|
|
39293
|
+
function readingSplits(reading) {
|
|
39294
|
+
if ("breakdown" in reading) return [["by", reading.by]];
|
|
39295
|
+
return "pivot" in reading ? [["rows", reading.rows], ["columns", reading.columns]] : [];
|
|
39296
|
+
}
|
|
39168
39297
|
function readingEntity(reading) {
|
|
39169
39298
|
if ("metric" in reading) return reading.metric;
|
|
39170
39299
|
if ("breakdown" in reading) return reading.breakdown;
|
|
@@ -39216,33 +39345,54 @@ function checkReading(model, reading, at2) {
|
|
|
39216
39345
|
if (found === void 0) findings.push(error51("model.names-declared", path18, `names "${name}", which is not a field of "${entity.alias}"`));
|
|
39217
39346
|
return found;
|
|
39218
39347
|
};
|
|
39348
|
+
const averaged = "metric" in reading && reading.average === true;
|
|
39219
39349
|
if ("value" in reading && reading.value !== void 0) {
|
|
39220
39350
|
const value = own2(reading.value, `${at2}.value`);
|
|
39221
39351
|
if (value !== void 0 && resolvedFieldType(value, entity, model.entities) !== "number") {
|
|
39222
39352
|
findings.push(error51("reading.value", `${at2}.value`, `"${reading.value}" is a ${value.type} field \u2014 a reading sums a number`));
|
|
39223
|
-
} else if (value !== void 0 && isPercent(contractNotation(value, entity, model.entities))) {
|
|
39353
|
+
} else if (value !== void 0 && !averaged && isPercent(contractNotation(value, entity, model.entities))) {
|
|
39224
39354
|
findings.push(error51("reading.value", `${at2}.value`, `"${reading.value}" is a percent, and percents do not add up \u2014 sum the amounts it is a share of`));
|
|
39225
39355
|
}
|
|
39226
39356
|
}
|
|
39227
39357
|
if ("metric" in reading && reading.target !== void 0 && reading.value !== void 0 && recordDisplay(model, alias2)?.limits?.[reading.value] !== void 0) {
|
|
39228
39358
|
findings.push(error51("reading.target", `${at2}.target`, `"${reading.value}" is read against its \`limits\` bound \u2014 the metric reads the sum of that bound; drop \`target\``));
|
|
39229
39359
|
}
|
|
39360
|
+
if ("metric" in reading && reading.average === true) {
|
|
39361
|
+
if (reading.value === void 0) {
|
|
39362
|
+
findings.push(error51("reading.average", `${at2}.average`, "averages no `value` \u2014 an average is of a number each row holds: state the field, or drop `average` to count the rows"));
|
|
39363
|
+
} else if (recordDisplay(model, alias2)?.limits?.[reading.value] !== void 0) {
|
|
39364
|
+
findings.push(error51("reading.average", `${at2}.average`, `"${reading.value}" is read against its \`limits\` bound, a sum over the rows \u2014 an average is no sum to read against it: average a value no limit bounds, or drop \`average\``));
|
|
39365
|
+
}
|
|
39366
|
+
}
|
|
39230
39367
|
if ("metric" in reading && reading.better !== void 0 && reading.over === void 0) {
|
|
39231
39368
|
findings.push(error51("reading.better", `${at2}.better`, "says which way its change is good, but it states no `over` \u2014 a figure changes over the periods its date places its rows in"));
|
|
39232
39369
|
}
|
|
39233
|
-
const
|
|
39234
|
-
|
|
39235
|
-
const
|
|
39370
|
+
for (const [part, split2] of readingSplits(reading)) {
|
|
39371
|
+
const name = filterField(split2);
|
|
39372
|
+
const banded = filterTiers(split2) !== void 0;
|
|
39373
|
+
const field = own2(name, banded ? `${at2}.${part}.field` : `${at2}.${part}`);
|
|
39236
39374
|
const read = field === void 0 ? void 0 : readField(model, entity, field);
|
|
39237
|
-
if (field !== void 0 &&
|
|
39375
|
+
if (field !== void 0 && banded) {
|
|
39376
|
+
const refused = bandsRefusal(model, entity, field);
|
|
39377
|
+
if (read?.several === true) {
|
|
39378
|
+
findings.push(error51("reading.split", `${at2}.${part}.field`, `"${name}" is a lookup of ${kindOf(read.field, read.entity, model.entities)} holding several values \u2014 a row is counted under one band: look it up through links to one row`));
|
|
39379
|
+
} else if (refused !== void 0) findings.push(error51("reading.split", `${at2}.${part}.tiers`, refused));
|
|
39380
|
+
} else if (field !== void 0 && (read === void 0 || read.several || !splitsOne(read.field, read.entity, model.entities))) {
|
|
39238
39381
|
const kind = read === void 0 ? kindOf(field, entity, model.entities) : kindOf(read.field, read.entity, model.entities);
|
|
39239
39382
|
const several = read?.several === true || ["select", "select_record_link", "select_member"].includes(read?.field.type ?? field.type);
|
|
39240
39383
|
const through = field.type === "lookup" ? `a lookup of ${kind}` : kind;
|
|
39241
|
-
|
|
39384
|
+
const number4 = !several && resolvedFieldType(field, entity, model.entities) === "number";
|
|
39385
|
+
findings.push(
|
|
39386
|
+
error51(
|
|
39387
|
+
"reading.split",
|
|
39388
|
+
`${at2}.${part}`,
|
|
39389
|
+
`"${name}" is ${through}${several ? " holding several values" : ""} \u2014 a row is counted under one option, one linked row, one person or yes/no${number4 ? `; a number splits by its bands: state \`{ "field": "${name}", "tiers": [...] }\`` : ""}`
|
|
39390
|
+
)
|
|
39391
|
+
);
|
|
39242
39392
|
}
|
|
39243
39393
|
}
|
|
39244
|
-
if ("pivot" in reading && reading.rows === reading.columns) {
|
|
39245
|
-
findings.push(error51("reading.split", `${at2}.columns`, `"${reading.columns}" is already the rows \u2014 a grid crosses two fields`));
|
|
39394
|
+
if ("pivot" in reading && filterField(reading.rows) === filterField(reading.columns)) {
|
|
39395
|
+
findings.push(error51("reading.split", `${at2}.columns`, `"${filterField(reading.columns)}" is already the rows \u2014 a grid crosses two fields`));
|
|
39246
39396
|
}
|
|
39247
39397
|
if ("over" in reading && reading.over !== void 0) {
|
|
39248
39398
|
const over = own2(reading.over, `${at2}.over`);
|
|
@@ -40301,8 +40451,8 @@ function registerFields(app) {
|
|
|
40301
40451
|
...readings.flatMap((reading) => [
|
|
40302
40452
|
..."value" in reading && reading.value !== void 0 ? [reading.value] : [],
|
|
40303
40453
|
..."over" in reading && reading.over !== void 0 ? [reading.over] : [],
|
|
40304
|
-
..."by" in reading ? [reading.by] : [],
|
|
40305
|
-
..."rows" in reading ? [reading.rows, reading.columns] : [],
|
|
40454
|
+
..."by" in reading ? [filterField(reading.by)] : [],
|
|
40455
|
+
..."rows" in reading ? [filterField(reading.rows), filterField(reading.columns)] : [],
|
|
40306
40456
|
...Object.keys(reading.where ?? {})
|
|
40307
40457
|
])
|
|
40308
40458
|
];
|
|
@@ -40707,8 +40857,7 @@ function resolveModelApp(given, written) {
|
|
|
40707
40857
|
}))
|
|
40708
40858
|
},
|
|
40709
40859
|
readings,
|
|
40710
|
-
...app.register?.attention === void 0 ? {} : { attention: app.register.attention }
|
|
40711
|
-
...app.register?.imports === void 0 ? {} : { imports: app.register.imports }
|
|
40860
|
+
...app.register?.attention === void 0 ? {} : { attention: app.register.attention }
|
|
40712
40861
|
},
|
|
40713
40862
|
record: { door, sections, comments: app.record?.comments === true, history: app.record?.history === true, ...moves === void 0 ? {} : { moves }, ...app.record?.activity === void 0 ? {} : { activity: app.record.activity }, ...app.record?.compose === void 0 ? {} : { compose: app.record.compose } },
|
|
40714
40863
|
acts,
|
|
@@ -42277,8 +42426,8 @@ function heldBy(narrowing) {
|
|
|
42277
42426
|
return Object.entries(narrowing ?? {}).map(([field, held2]) => ({ field, values: Array.isArray(held2) ? held2.length : 1, me: held2 === NARROW_ME }));
|
|
42278
42427
|
}
|
|
42279
42428
|
function modelPressed(reading) {
|
|
42280
|
-
if ("breakdown" in reading) return { kind: "breakdown", by: reading.by, where: heldBy(reading.where) };
|
|
42281
|
-
if ("pivot" in reading) return { kind: "pivot", rows: reading.rows, columns: reading.columns };
|
|
42429
|
+
if ("breakdown" in reading) return { kind: "breakdown", by: filterField(reading.by), where: heldBy(reading.where) };
|
|
42430
|
+
if ("pivot" in reading) return { kind: "pivot", rows: filterField(reading.rows), columns: filterField(reading.columns) };
|
|
42282
42431
|
if ("metric" in reading) return { kind: "metric", where: heldBy(reading.where) };
|
|
42283
42432
|
return { kind: "trend" };
|
|
42284
42433
|
}
|
|
@@ -42305,6 +42454,8 @@ function validateModelApps(stated) {
|
|
|
42305
42454
|
const at2 = `apps.${app.alias}`;
|
|
42306
42455
|
if (seen.has(app.alias)) findings.push(error51("app.alias-unique", `apps.${index}.alias`, `"${app.alias}" names two apps \u2014 an app alias is unique`));
|
|
42307
42456
|
seen.add(app.alias);
|
|
42457
|
+
if (!isValidAppIcon(app.icon)) findings.push(error51("app.icon", `${at2}.icon`, `"${app.icon}" is no Lucide icon \u2014 name one in kebab-case, as "receipt" or "trending-up"`));
|
|
42458
|
+
findings.push(...checkWorkflows(model, app, at2));
|
|
42308
42459
|
if (isDashboardApp(app)) {
|
|
42309
42460
|
const own3 = checkDashboard(model, app, at2);
|
|
42310
42461
|
findings.push(...own3);
|
|
@@ -42590,7 +42741,23 @@ function checkApp(model, app, at2) {
|
|
|
42590
42741
|
if (tab !== void 0 && !named.has(tab)) {
|
|
42591
42742
|
findings.push(error51("register.tabs", `${path18}.tab`, named.size === 0 ? `reads the tab "${tab}", and the register has no tabs` : `reads the tab "${tab}", which is none of the register's: ${[...named].join(", ")}`));
|
|
42592
42743
|
}
|
|
42593
|
-
|
|
42744
|
+
for (const [part, split2] of readingSplits(reading)) {
|
|
42745
|
+
const tiers = filterTiers(split2);
|
|
42746
|
+
if (tiers === void 0) continue;
|
|
42747
|
+
const name = filterField(split2);
|
|
42748
|
+
const filter2 = (app.register?.filters ?? []).find((one) => filterField(one) === name);
|
|
42749
|
+
const filtered = filter2 === void 0 ? void 0 : filterTiers(filter2);
|
|
42750
|
+
if (filtered !== void 0 && filtered.length === tiers.length && filtered.every((tier, at3) => tier === tiers[at3])) continue;
|
|
42751
|
+
const state = `\`{ "field": "${name}", "tiers": [${tiers.join(", ")}] }\``;
|
|
42752
|
+
findings.push(
|
|
42753
|
+
error51(
|
|
42754
|
+
"register.readings-bands",
|
|
42755
|
+
`${path18}.${part}`,
|
|
42756
|
+
filtered === void 0 ? `bands "${name}", and a press on a band narrows the rows to it \u2014 state ${state} in the register's \`filters\`${filter2 === void 0 ? "" : `, in place of "${name}" bare`}` : `bands "${name}" at ${tiers.join(", ")}, and the register's filter bands it at ${filtered.join(", ")} \u2014 a press on a band narrows the rows by that filter: state the same tiers in both`
|
|
42757
|
+
)
|
|
42758
|
+
);
|
|
42759
|
+
}
|
|
42760
|
+
if ("metric" in reading && reading.value !== void 0 && reading.average !== true && reading.value === display.figure && reading.where === void 0) {
|
|
42594
42761
|
findings.push(error51("register.readings-restate", path18, `sums "${reading.value}", the register's figure, over the rows in view \u2014 the foot states that total; give it a \`where\` the foot does not carry, or drop it`));
|
|
42595
42762
|
}
|
|
42596
42763
|
if ("metric" in reading && reading.value === void 0 && reading.where === void 0 && display.status !== void 0) {
|
|
@@ -42841,7 +43008,7 @@ function checkFilter(model, entity, stated, path18, around) {
|
|
|
42841
43008
|
const kind = read === void 0 ? `a ${filter2.type} field` : `a lookup of a ${read.field.type} field`;
|
|
42842
43009
|
return [error51("register.filter", path18, `"${name}" is ${kind} \u2014 ${around.owner} narrows by a select, a member, a link, a date, a yes/no or a number`)];
|
|
42843
43010
|
}
|
|
42844
|
-
if (typeof stated !== "string") return checkTiers(model, entity, filter2,
|
|
43011
|
+
if (typeof stated !== "string") return checkTiers(model, entity, filter2, `${path18}.tiers`);
|
|
42845
43012
|
if (around.splits.has(name) && type !== "select_member") {
|
|
42846
43013
|
return [note("register.filter-split", path18, `"${name}" is also what a picture above the rows splits them by \u2014 a press on its part narrows by it`)];
|
|
42847
43014
|
}
|
|
@@ -43274,17 +43441,11 @@ function checkGantt(model, entity, app, at2) {
|
|
|
43274
43441
|
const shape = ganttShape(model, entity.alias, gantt);
|
|
43275
43442
|
return "refused" in shape ? shape.refused.map((one) => error51("register.gantt", one.at === "" ? path18 : `${path18}.${one.at}`, one.reason)) : [];
|
|
43276
43443
|
}
|
|
43277
|
-
function checkTiers(model, entity, field,
|
|
43278
|
-
|
|
43444
|
+
function checkTiers(model, entity, field, path18) {
|
|
43445
|
+
const refused = bandsRefusal(model, entity, field);
|
|
43446
|
+
if (refused === void 0) return [];
|
|
43279
43447
|
const select = contractFigureSelect(field, entity, model.entities);
|
|
43280
|
-
|
|
43281
|
-
return [
|
|
43282
|
-
error51(
|
|
43283
|
-
"register.tiers",
|
|
43284
|
-
path18,
|
|
43285
|
-
`"${field.alias}" is read in each row's own ${select.vocabulary} ("${select.alias}"), so a breakpoint is a different amount on each row \u2014 name it bare: its range is typed in the ${select.vocabulary} the reader picks`
|
|
43286
|
-
)
|
|
43287
|
-
];
|
|
43448
|
+
return [error51("register.tiers", path18, select === void 0 ? refused : `${refused}: its range is typed in the ${select.vocabulary} the reader picks`)];
|
|
43288
43449
|
}
|
|
43289
43450
|
function checkReports(model, entity, app, status, at2) {
|
|
43290
43451
|
const reports = app.register?.export;
|
|
@@ -43823,7 +43984,7 @@ function validateModelRows(model, rows) {
|
|
|
43823
43984
|
errors.push(error51("model.names-declared", path18, `is not a field of entity "${alias2}" \u2014 a row's keys are field aliases (${fieldByAlias.size > 0 ? [...fieldByAlias.keys()].join(", ") : "this entity declares none"})`));
|
|
43824
43985
|
continue;
|
|
43825
43986
|
}
|
|
43826
|
-
|
|
43987
|
+
checkValue2(field, value, path18, refs, errors);
|
|
43827
43988
|
}
|
|
43828
43989
|
for (const field of entity.fields) {
|
|
43829
43990
|
if (field.required !== true || COMPUTED_FIELD_TYPES2.has(field.type)) continue;
|
|
@@ -43835,7 +43996,7 @@ function validateModelRows(model, rows) {
|
|
|
43835
43996
|
return errors;
|
|
43836
43997
|
}
|
|
43837
43998
|
var COMPUTED_FIELD_TYPES2 = /* @__PURE__ */ new Set(["formula", "rollup", "lookup", "autonumber"]);
|
|
43838
|
-
function
|
|
43999
|
+
function checkValue2(field, value, path18, refs, errors) {
|
|
43839
44000
|
switch (field.type) {
|
|
43840
44001
|
case "select": {
|
|
43841
44002
|
const declared = new Set(field.options.map((option) => option.alias));
|
|
@@ -44350,8 +44511,8 @@ async function saveSignedLinks(links, dir) {
|
|
|
44350
44511
|
}
|
|
44351
44512
|
|
|
44352
44513
|
// src/version.ts
|
|
44353
|
-
var VERSION = "0.
|
|
44354
|
-
var APP_SDK_VERSION = "0.
|
|
44514
|
+
var VERSION = "0.303.0";
|
|
44515
|
+
var APP_SDK_VERSION = "0.120.0";
|
|
44355
44516
|
|
|
44356
44517
|
// src/timezone.ts
|
|
44357
44518
|
function machineTimezone() {
|
|
@@ -44370,7 +44531,7 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
44370
44531
|
var design_default = "# Designing an app\n\nStart here to build an app. This page is the method and the choice of treatment for each thing a job\nreads; every key it names is a key of `model.json`, and the page of the `model` reference stating a\ntreatment's keys stands beside it in \xA7 Treatments. A treatment worked through in an app of a complete model\nis a page at `examples/<treatment>` \u2014 `lotics docs`, or the `docs` tool.\n\nA model states COMPOSITION \u2014 which fields, lists and acts each job's screen holds. How each piece looks is\nthe platform's, one way per concept, and no key changes it. What the model leaves unnamed is never drawn,\nand a list the model leaves as a table is drawn as one, even where a calendar, a face or a picture would\nanswer the reader at a glance. That is the most common failure of a model, so the apply refuses an app leaving\nout a treatment it requires \u2014 a register's readings, a record's sections and children, its status moves, a picture,\nrows in time, a template's act \u2014 until the model states it or declines it.\n\n## The method\n\n1. **People and jobs, before any table.** Name each person by the work they do, then each JOB in one\n sentence: who does it, what exists when it is done, the record it moves. The sentence is the app's\n `name` and `description`. A job is a desk a person works every day from its own list; one app per job,\n each one register over one entity \u2014 an app per table, or one app holding every table, is no job's screen.\n A list worked only in passing on the way through another job \u2014 the rates a quote is priced from, the\n transfers its receipts are matched to, the statement sent from its records \u2014 is a block, a section or an\n act of that job's app, never an app of its own. A small business usually lands at four to six.\n2. **The entities the jobs touch.** A status is a state the work sits in, six options or fewer: a touch\n (\"called\") is a row of a log, a money fact (\"paid in full\") a formula the figure shows. A quantity is a\n number with its `unit`, never words in a text field; a quantity of several kinds is child rows, one per\n kind, totalled at the foot of their block.\n3. **How each row is recognised** \u2014 a `records` entry for every entity an app lists, opens or picks: its\n `title` (who or what it is, never a code \u2014 the code is a `subtitle`), up to two `subtitle` fields the job\n reads at first glance, its `image`, its `party`, its `status` and which options are `closed`, its\n `figure`, its `due` dates.\n4. **Each app's composition** \u2014 the register's `columns` in the order the job reads them, as many as fit a\n desk (one that does not fit is drawn nowhere in the row; what is read only on the way deeper is the\n record's), the `filters` it narrows by every day (at most 3, each a field the rows show), its `layout`\n and `readings`; the record's `sections` in the order the work reaches them, each one's `fields`, `blocks`\n and `acts`; the `checks` that warn or refuse. A rule the business states \u2014 a field needed before an act,\n a quantity bounded, a picker narrowed, a duplicate reused \u2014 is the model's (an act's `requires`, a check's\n `blocks`, `write_rules`), and every write re-checks it; a check without `blocks` only warns.\n5. **A treatment for every list and every entity** \u2014 the next section. Choose it from what the rows ARE,\n before writing any register.\n6. **Plan, apply, look.** The `check_model` tool (`lotics model check` on the CLI) checks the\n whole model beside the workspace's rows and writes nothing \u2014 the patch adopting each app's decisions in\n `designs`, and the app's body as they leave it its `draft`; `apply_model` (`lotics model apply`) mints each app live. Then look at each app \u2014\n \xA7 Look at it \u2014 and judge it against \xA7 The visual bar. A correction is a change to the model, applied again.\n\n## Treatments\n\nA finding naming a section below is a treatment the model left out. A decided one refuses the apply, and the\ncheck gives each app it refuses a patch stating its decided treatments: merge the patches in the order the check gives\nthem, or state why the app stays as it is in its `declines` (`model/declines`), under the key the finding names \u2014\nthen check again. A note refuses nothing.\n\n| The rows are | The treatment | Its keys |\n|---|---|---|\n| People or organisations | `records.<entity>.party`, with a photo or logo as its `image` | `model/records` |\n| Things with a photo, a scan or a paper | `records.<entity>.image`; `layout: \"cards\"` where the picture is how a row is found | `model/records`, `model/cards` |\n| Read by their day | `layout: \"calendar\"`; on a record, an `agenda` block | `model/calendar`, `model/timeline-and-agenda` |\n| Booked on a resource | `layout: \"lanes\"` with `lanes`, and `write_rules` `no_overlap` | `model/lanes`, `model/write-rules` |\n| A plan of spans | `layout: \"gantt\"` with `gantt` | `model/gantt` |\n| Who did what on which day | `layout: \"roster\"` with `roster` | `model/roster` |\n| A log of what happened | a `timeline` block | `model/timeline-and-agenda` |\n| Opened to be worked on | the record's `sections`, in the order the work reaches them \u2014 `at` where it moves in steps | `model/record-page` |\n| A record whose page is its own arrangement of pieces the app already states | `record.compose` | `model/composed-page` |\n| Rows that each belong to another row | a `rows` block on that row's record | `model/blocks` |\n| Moving through a status | an act per move, `status.history`, and the register's `readings` | `model/acts`, `model/status`, `model/readings` |\n| Steps people drop out of between | a `funnel` block on the record whose child rows count them | `model/reading-blocks` |\n| A record's own numbers nobody keys, read at a glance | a `figures` block: two to eight of them as tiles, one holding nothing read as not measured | `model/reading-blocks` |\n| Counted over a period | the register's `tabs` over one `period`; an `open` tab's `where` reads only what was true at the period's end, never a status the row moves on from | `model/tabs` |\n| Papers a case gathers | a rows block with `expect`; the papers made, an act's `templates` | `model/blocks`, `model/papers-and-recordings` |\n| Rows that are each a place a record is sent | a rows block with `expect` on the link to those places and `post`: each ticked, the post written there and read as its feed shows it | `model/publish-desk` |\n| Papers an agent reads, where the owner asked for one | an act's `intake` with `fills` | `model/papers-and-recordings` |\n| Calls and visits | an act's `record` | `model/papers-and-recordings` |\n| Documents covering lines | a rows block with `under` | `model/blocks` |\n| Rows on file a record takes in | a rows block with `pick` | `model/blocks` |\n| One owner's question across jobs | a dashboard app | `model/dashboards` |\n\n### Faces and pictures\n\n#### Party\n\n`records.<entity>.party` is `\"person\"` where the rows are people (a contact, a patient, a driver) and\n`\"organization\"` where they are companies (a customer, a supplier, a carrier). Every surface drawing one of\nits rows \u2014 the register's row, a link's chip, a picker's option, the record's header \u2014 then draws a face:\nits `image` (a photo, a logo), else its initials. Without it, a person is drawn as a thing. A workspace\nmember is a `select_member` field, and is drawn by their face already. Worked: `examples/party`.\n\n#### Image\n\n`records.<entity>.image` names the files field that pictures a row \u2014 a product's photo, a damage photo, a\nscan, a receipt. Every row of it then leads with its picture, or its paper's preview, wherever it is drawn;\na link to it previews the file. Never a field an act keeps the paper it makes in (`into`). Register\n`layout: \"cards\"` where the picture is how a row is found. A files field the record keeps as a folder of\npapers is a `files` block, read whole. Worked: `examples/cards`.\n\n#### A link opens its row\n\nOn a record, a link to one row opens that row's peek: a drawer on a desk, a sheet on a phone, reading the\nrow's face \u2014 its mark, its name, its status and the line under it \u2014 and nothing to edit. A link read in a\nrow's cell or on its line opens from its name; a link the record keeps as a field rests as its picker, and\nits way out stands at the field's label where the row has somewhere to go. The peek's one act leaves for\nthe app whose register is that entity's rows \u2014 the first such app, in the model's order, that the member\ncan open \u2014 at the row's own address, or goes to the row inside this app where the entity is the app's own\nor one of its books'. With no such app the peek has no act, and a link answered in what an act asks opens\nno peek. The model states none of it: give the entity an app of its own and\nits links lead there. The app opened decides what the member reads of the row; where it keeps the row\nfrom them, they land on its not-found.\n\n### Rows in time\n\nRows that each stand on a day, or hold a run of days or hours, are read where they stand in time \u2014 never\nas two date columns. A row holds a span when its `records` line (title and subtitle) holds two dates, its\n`records.frees` names the day it ends, or `write_rules.<entity>.no_overlap` books it on a row its `by` links to (a\nchair, a machine). A `no_overlap` by a person or a value alone keeps one row at a time each \u2014 a member's periods in\nturn \u2014 read by what they hold.\n\n#### Calendar\n\n`register.layout: \"calendar\"`: the rows by their day \u2014 a month, a week, a day or a list; an entry at its\nhour where its date holds one, over its span where its line holds a second date. An entry pressed opens a card on\nit: the row, Delete, its acts, Reschedule (the day alone moves; its hour and its length stay) and Open. Worked:\n`examples/calendar`.\n\n#### Lanes\n\n`register.layout: \"lanes\"` with `lanes` naming a one-row link whose target's rows are the lanes (a chair, a\nmachine, a room), each drawn even when empty: every row a block from the first date on its line to the\nsecond, and a free stretch adds a row there. `loads` reads what a lane carries against what it holds. A\n`no_overlap` `by` the same link refuses two rows on one resource at once. Worked: `examples/lanes`.\n\n#### Gantt\n\n`register.layout: \"gantt\"` with `gantt.start` (and `end`, else the entity's first `due`): one bar a row over\nits run of days \u2014 a shipment, a hire, a task of a project \u2014 read against today. `gantt.milestones` marks\ndates on the bar, `planned` the plan under it, `progress` how far it is done, `after` the rows it waits on;\nthe register's `group` is its lanes. Worked: `examples/gantt`.\n\n#### Roster\n\n`register.layout: \"roster\"` with `roster` naming the child whose rows fill the days \u2014 one link back to the\nregister's entity and a date on its line: what each row did on each day (shifts, runs, attendance). With\n`expect` (and `of`) the columns are the options of a select instead of days: which papers each row holds.\nWorked: `examples/roster`.\n\n#### Agenda\n\nAn `agenda` block on a record: a child's planned entries by day, soonest first, today marked, each with its\npicture; within a day by the hour, or by a single select of the part of the day. `start` counts Day 1 from a\ndate of the record (a trip, a course).\n\n#### Timeline\n\nA `timeline` block on a record: a child read as a log of dated entries, newest first, with a composer \u2014 the\ncalls, visits and notes about the record, each with who and when, and its files where the child's `image`\nnames them. Entries planned ahead are an `agenda`; the status's moves are its history. Worked:\n`examples/party`.\n\n### Moves and numbers\n\n#### Moves\n\nRows a status stages towards its `closed` options move by acts: each moves one option on to the next (`when` the\nstatus holds it, `set` to the next), at the foot of the section whose work it ends. A status with nothing closed is a\ncondition a person sets. A value decided as a row reaches an outcome \u2014 why it was lost, refused, voided \u2014 is that\nact's `asks`, never a field the reader must remember to fill. An act reaching past the workspace \u2014 sending an invoice\nto a service, a letter by mail \u2014 runs its `workflow`, and what the workflow keeps from the answer (an invoice's id,\nits link) is named in `writes`, the act's as what it sets is. A stage the work only steps aside into from one step\nand leaves only back to it \u2014 a pause, a repair \u2014 is a detour: its option and its section stand before that step,\nentered by its own act; listed after, it reads as the work's next step.\n\n#### Stages\n\nName each job's work plan by its shape: a status a person sets; a pipeline, where who holds the row and for how long\nmatter; or tasks, where only how many are done counts (`records.<entity>.task`). A pipeline states each stage once in\n`records.<entity>.status.stages` \u2014 its `owner` (a member field, a link's member field, or a `role`) and its target\n`within` days \u2014 and the server holds it: only the owner's acts move the row on, and a level an amount skips stands\nnamed with why. Late rows, rows due soon, a warning the line draws and rows waiting on the reader stand first in a\nregister; a register whose job is a strict order, a first-come queue, states `register.attention: false`.\n\n#### History\n\n`records.<entity>.status.history` names a child the app's own writes append one row to per status move: its\nlink to the row, the new status, the moment and who. It keeps when each row entered each status and who\nmoved it \u2014 what the rail reads. A record's activity shows every move as a change, so `record.history: true` adds\nnothing \u2014 and beside `record.activity: false`, which shows them nowhere, it is\nrefused. Worked: `examples/approval`.\n\n#### Readings\n\n`register.readings` stand above the rows and read the rows in view. Lead with the job's headline number \u2014\nits exception as a formula's yes/no in a `metric`'s `where` (what is late, what is short), never a stage the\nstatus chips already count \u2014 then at most the mix (`breakdown`) and the movement (`trend`), varied by job.\nA `pivot` reads on a dashboard. A dashboard app (`dashboard`) answers one owner's question across jobs, each\nreading windowed by the period the reader picks, and a `list` reading hands over the rows to act on. A\nrecord's `metric`, `breakdown` and `trend` blocks read its own child rows. Worked: `examples/approval`, `examples/dashboard`.\n\n#### Funnel\n\nA `funnel` block reads a record's child rows as steps, each a number column summed over them \u2014 a week's channels\nfrom impressions to bookings, an order's lines from ordered to shipped. It states how much of the first step\neach later one keeps, the share lost between two neighbours, and the last over the first. State two to eight\n`steps` in one unit; where the path turns \u2014 reach, then conversion \u2014 state two blocks one straight after the\nother, so the second's bars are read over its own first step: they draw on one grid, every bar starting at one\nedge and every value ending at another, each under its own label. A step no row holds a number in reads as not measured, so leave a count empty\nrather than typing a zero nobody measured.\n\n### The record\n\n#### Sections\n\n`record.sections`, in the order the work reaches them: the fields people write, a `files` block per paper the record\nkeeps, a block per child, the acts at each one's foot. A register laid out in time (calendar, lanes, roster) opens the\ndefault record: every field a person writes. `record.compose` is not for a record a standard section order would\ndo, and never for behaviour, which belongs in an act.\n\n#### Children\n\nRows that each belong to one row of another entity (a one-row link to it) are drawn under that row: a `rows` block on\nits record, a `timeline` where they are a log, an `agenda` where they are planned on a day and within it, a roster of\nthem, or lanes of them by that link. Which children a record lists is the author's \u2014 one of them at least. A block\nlisting only some of them narrows by its `where`: a select (one leg's fees), or a yes/no \u2014 a formula's for what is\nstill owed \u2014 on a block that adds no rows. Rows on\nfile before the row they come to belong to \u2014 a job's cost lines before the supplier bill over them \u2014 are picked into\nit: a rows block with `pick`, often with `create: false` and `remove: false`.\n\n### Papers\n\n#### Expect\n\nA rows block with `expect` naming the child's title \u2014 a single select of kinds, or a one-row link to a\ncatalog: one line per kind, the ones not yet filled standing empty, filled in place. `of` narrows the kinds\nto the record's own multi-select of those it needs. The papers a case gathers, the fees a record knows it\nowes. Worked: `examples/case`.\n\n#### Templates\n\nA paper the business makes is an act's `template`, kept in the record's files field `into` \u2014 or several at\nonce, `templates`: listed where the act stands, each made one as its file and each not yet made as a\nplaceholder, the reader ticking which to make. A template no act names is made by nothing. Worked: `examples/case`.\n\n#### Intake\n\nA paper the business is brought and reads is an act's `intake`: the press takes the papers, an agent reads\nthem, and each is filed as the line of its kind in the child the record `expect`s, while `fills` writes what\nthey state into the record's fields \u2014 a single link as a row the agent finds among those the app reads of its\nentity \u2014 shown before it is saved. Worked: `examples/case`.\n\nAn agent act \u2014 an `intake`, a `record`'s `fills` \u2014 spends a run on every press and writes what the record\nsays, so it is stated only where the owner asked for an agent to do that job; otherwise a paper brought is its\nline of the `expect` block, and a person types what it states.\n\n#### Under\n\nA rows block with `under`: the documents that cover lines (an invoice over its fee lines) stand as headings\nover the lines they cover, and a line no document covers offers to make one. A document entered as it arrives,\nover lines already on file (a supplier bill), picks them from its own record instead: a rows block with `pick`.\nChoose one per document: a document a make covers lines by is never also picked into, since a pick does not check\nwhat the make does.\n\n### Calls\n\n#### Record act\n\nAn act's `record`: the press records a call, visit or meeting \u2014 its audio, its transcript, its screen where\ncaptured \u2014 and files it as a new row of the record's `timeline` child (`into`), stamped `at` and `by`;\n`fills` lets an agent fill that row's fields from the transcript, where the owner asked for one. Worked: `examples/calls`.\n\n## The visual bar\n\n- A person shows a face, a thing its picture, a file its preview, a plan its days on a time axis, a\n quantity a meter (`records.limits`) or a chart, a register its `readings` under its title, and long text\n reads whole (a `text` block).\n- Every category the job reads is coloured: each select option's `color`, and a `mark` where a brand or a\n glyph names it \u2014 on every option of a select read down a column (how it was paid, the channel, the mode).\n- Someone is a face wherever they are read. A person is a member field and a party a row of an entity stating\n `party`; where a select's options name people or organisations who are neither, every option's `mark` is\n its face (`{\"kind\": \"avatar\", \"name\": \"person\"}`), never a colour dot.\n- Every files field is drawn somewhere \u2014 a section's `fields`, a `files` block, the `image`, or where the\n act making it stands.\n- The paperwork is the proof of the work, so every paper the job takes in and every document it issues has a\n place on the record, in the section where that work happens: a files field per purpose (what arrived, what\n was issued to the customer, to the carrier, to the authority), each kept visible after the stage that made it,\n and a source \u2014 a screenshot, an email, a sheet \u2014 on the row whose figure came from it.\n- A screen with none of these is bland, and bland is a finding: text drawn where a treatment above exists\n is a change to the model.\n\n## Look at it\n\nAfter an apply, the `screenshot_app` tool draws each app as its people see it \u2014 the register, and a record\nby its `path` \u2014 at a desk's width and a phone's, one screen a shot, naming the columns a table left off at\neach width: `screen` names which screen of a long record. From the CLI, `-o <dir>` saves the shots. Beside the shots, its `verdict` is what a plan of the workspace's\nmodel finds of the app now, beside its rows: what the next apply would refuse it for and the notes on it, the\n`patch` adopting the decisions among them, and the app's body as the patch leaves it (`draft`). Judge each shot\nagainst \xA7 The visual bar, change the model, and apply again. The `rollback_app` tool returns an app to an earlier\nversion; the tables and rows stay as they are.\n";
|
|
44371
44532
|
|
|
44372
44533
|
// src/model_reference.md
|
|
44373
|
-
var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOne JSON file describing a workspace: its tables, fields, options, views, roles,\nfirst rows, how a row of each table is recognised, and the apps over them. Each \xA7\nis its own page at `model/<section>` \u2014 `lotics docs`, or the `docs` tool. Which treatment each job\'s\nscreen needs: the `design` reference. Each treatment worked through in an app of a complete model: the\n`examples` reference.\n\n**What a model composes with**\n\n- **Entities and fields** (\xA7 Entity, \xA7 Field) \u2014 the tables, their columns, options and links.\n- **Records** (\xA7 Records, then \xA7 Status) \u2014 how a row of each entity is RECOGNISED: its title, the line\n under it, its picture, its status, the one number it stands for. Stated once per entity\n and read by every surface that draws one of its rows.\n- **Write rules** (\xA7 Write rules) \u2014 what a write finds, bounds and picks among.\n- **Apps** (\xA7 Apps, then a page each: \xA7 Register, \xA7 Layouts, \xA7 Tabs, \xA7 Export, \xA7 Readings,\n \xA7 Dashboards, \xA7 Record page, \xA7 Blocks, \xA7 Composed page, \xA7 Publish desk, \xA7 Timeline and agenda, \xA7 Statements, \xA7 Reading blocks, \xA7 Tasks, \xA7 Acts,\n \xA7 Papers and recordings, \xA7 Checks) \u2014 one\n register over one entity and the record each row opens: which fields go where, the acts,\n and the checks that guard them \u2014 or a dashboard of readings.\n How each thing LOOKS is the runtime\'s, one treatment per concept; no key here changes it.\n A screen the job needs and no key states is a `lotics report` \u2014 the job, what the model\n drew, the word wanted \u2014 never keys bent to approximate it.\n\n**The working order**\n\n1. Name the people and each one\'s JOB \u2014 the work they alone decide or write.\n2. The entities and fields those jobs touch, and a `records` entry for every entity an\n app lists, opens or picks.\n3. One app per job in `apps[]` \u2014 a desk a person works daily from its own list \u2014 each one register over one entity; a list worked only in passing is a section of the app it serves.\n4. `lotics model apply model.json` \u2014 the file is checked first, every problem in one run;\n then the tables, then a new version of every app, live\n (`lotics setup model.json --email you@company.com` where no account exists yet).\n5. Change the file and apply it again; `lotics model pull -o model.json` writes what the\n workspace holds, and each apply of that file sends only what it changes. `apply_model` takes\n a `patch` in place of `model`: a JSON merge patch (RFC 7396) over the workspace\'s model, each\n list whose items carry an `alias` written as an object keyed by it \u2014\n `{ "apps": { "desk": { "acts": { "close": null } } } }` removes one act. Where an aliased item\n stands is a move beside the patch, `"moves": [{ "at": "apps.desk.acts.close", "after": "hold" }]`\n (or `"first": true`), placed once the patch merged \u2014 built into an app\'s acts, and a select\'s\n options (`entities.<entity>.fields.<field>.options.<option>`) reordered on its table, the plan\n saying each order it writes; a table\'s\n fields and views and the apps themselves keep the workspace\'s order. A patch whose every key and\n move lies inside one app\'s body leaves the tables as they stand and builds that app, beside any\n other app whose build reads what it changed. A `null` removes only\n what an apply writes as the model states it \u2014 anything inside an app, a `records` entry\n (`"records": { "<entity>": null }`), a write rule, an entity\'s `singular` and `writes`, a\n field\'s `default`, `required` and `writes`, a text field\'s `format`, a number\'s `notation` \u2014 and is refused\n anywhere else, a table, field, option, view, role, template or app among them; one naming\n nothing the model holds changes nothing. `get_model` with `app` reads one app and the\n entities it reads.\n Rolling an app back (`lotics run rollback_app`) restores its earlier version \u2014 table\n changes and data writes stay.\n\n## The rules\n\n- **At least one entity, at most 50.** More tables than that is a data model\n being designed, not applied \u2014 apply the rest in a second call.\n- **An existing table is adopted.** `lotics model apply` binds an entity whose `label`\n already names a table in the workspace, and adds the fields, options and\n views it is missing. No stored value is ever changed or deleted: a field it\n adopts takes the model\'s `default` and `required`, a text field\'s `format`, and\n the `notation` it reads in, where that only relabels \u2014 a date\'s format and a unit\n converting its figures are left. A field the model once stated and no longer does\n stays, with its rows, and is no longer required. A computed field\'s definition follows the model: a formula\'s expression,\n notation and options, a rollup\'s link, aggregation and filter, a\n lookup\'s link, field and order, and an autonumber\'s `template` (new rows only \u2014\n existing numbers keep theirs). A check and the apply name each one that changes.\n Applying the same model twice changes nothing the second time.\n- **Renaming a field is `lotics run update_table`**, then the same label in this\n file; a table is deleted on the CLI by `lotics run delete_table`, or by a member\n in Lotics. Neither goes through the file (\xA7 What a run remembers).\n- **The file\'s own majority is the language.** A model names no locale \u2014 which\n language it is in is what it mostly says, and `model apply` notes the label\n written the other way. The generated screens read the kit\'s pack, and a\n generated WRITE cannot: its refusals run on the server, so they are worded in\n that same majority. Mix the two and the workspace answers in two languages.\n- **Rows land only where every bound table is empty.** One table already holding\n records and no rows are written anywhere: sample rows landing among a\n customer\'s real ones cannot be told apart from them.\n- **`lotics model apply` checks all of it before anything is written**, and\n reports every problem in one run rather than the first. Each section of this\n page ends its keys with the rules the check enforces there, one sentence each\n under an id; a finding names its rule\'s id (`[register.filter]`), and\n `model/<rule id>` is that one rule\'s page.\n\n<!-- generated:start rules-model -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `model.schema` | Every key is one its table lists, holding the type its row gives, and every required key is stated. |\n| `model.retired` | `field_roles`, `table_workflows` and `connections` are no longer part of a model: `records` states how a row is recognised, and an app\'s own writes do what a table automation did. |\n| `model.names-declared` | Every alias a key names is declared: an entity of this model, a field of the entity the key reads, an option of the select it names, an act of the app, a template or an app of this model. |\n| `model.tables` | A model declares at least one table and at most 50; apply the rest as a second model. |\n| `model.rows-cap` | Rows are a sample: at most 200 per entity, 2000 per model and 2000 documents attached \u2014 a real data set belongs in an import. |\n| `model.language` | *Noted, never refused:* A label or description written in the other language than the model\'s majority \u2014 with or without diacritics. |\n| `model.alias-unique` | An alias is unique where it is named: entities, roles and templates in the model, fields and views in their entity, options in their select. |\n| `model.label-unique` | A label is unique where apply finds it by label: entities, roles and templates in the model, fields and views in their entity, options in their select \u2014 and no view takes its entity\'s own label, which names the whole-table grid apply makes. |\n| `model.template-sha` | A template\'s `content_sha256`, where stated, is the 64-character lowercase hex sha256 of its content. |\n| `model.template-kind` | An act\'s paper and a register\'s `export` are made from an html or an excel template, never an email one. |\n| `model.template-act` | *Refused until adopted or declined:* In a model stating apps none of whose acts runs an authored `workflow`, every html or excel template is made by an act (`template`, `templates`) or filled by a register\'s `export` \u2014 the patch adds the act to the app over the entity whose fields it prints most. |\n| `model.filter` | A filter \u2014 a view\'s, a rollup\'s \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares. |\n\n<!-- generated:end rules-model -->\n\n## Top level\n\nEvery table of keys on this page is generated from the schema `model apply`\nparses the file with, so it is the whole of what a key may hold.\n\n<!-- generated:start top-level -->\n\n#### Model file\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entities` | list of [Entity](#entity) | yes | The tables this model creates, with their fields, options and views |\n| `roles` | list of [Role](#role) | no | Workspace groups to create; members are added to them afterwards |\n| `templates` | list of [Template](#template) | no | Document templates: html and email inline, excel made from an uploaded workbook |\n| `rows` | map of alias \u2192 list of [Row](#row) | no | First records, keyed by entity alias \u2014 written only where every table they land in is empty |\n| `records` | map of alias \u2192 [Record](#record) \\| `null` | no | How a row of each entity is recognised, keyed by entity alias. Every entity an app lists, opens or picks has one; `null` removes the entry its table holds |\n| `write_rules` | map of alias \u2192 [Entity write rules](#entity-write-rules) | no | Entity alias \u2192 what a write of that entity finds, bounds and picks among |\n| `apps` | list of ([App](#app) \\| [Dashboard app](#dashboard-app)) | no | The apps this workspace will have \u2014 each one register over an entity, or a dashboard of readings |\n\n<!-- generated:end top-level -->\n\n**A model carries no** `fixtures`, `knowledge` or `knowledge_expects`, and no\n`word` / `pdf-form` template: file content is uploaded to the workspace, never\nstated in a model \u2014 an `excel` template names its uploaded workbook by `file_id`. `apps` here is what an agent states to make an app,\nnever built code. An unknown top-level key is an error, never ignored.\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how the file cross-references\nitself; they are never shown to anyone. `label` is what a person sees.\n\nLabels must be unique within their namespace \u2014 two entities, two fields on one\nentity, two options on one field, two views on one entity, two roles or two\ntemplates cannot share a label, because `apply` matches by label.\n\n## Entity\n\n<!-- generated:start entity -->\n\n#### Entity\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable entity alias, unique within the contract |\n| `label` | text | yes | The table\'s name in the workspace, which apply names it |\n| `singular` | text | no | One row of this table, in the business\'s own words \u2014 what a create\'s button and panel name |\n| `description` | text | no | The table\'s description, written onto the table in the workspace |\n| `writes` | `false` | no | false: no app opens or edits this table\'s rows. Direct writes follow the table\'s access, as on any table |\n| `fields` | list of [Field](#field) (at least one) | yes | The table\'s columns |\n| `read_scope` | [Read scope](#read-scope) | no | Which rows a member reads. Absent, every member with access to the table reads every row. |\n| `unique` | list of list of alias (at least one) (at least one) | no | Sets of fields whose values no two live rows share \u2014 each a list of field aliases (text, number, date, a single select, or a link of cardinality "one"). A create or update landing a second row with the same values is refused. |\n| `views` | list of [View](#view) | no | Saved views, in the order they are listed; with none, the table still opens on its default grid |\n\n#### Read scope\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `any` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option)) (at least one) | yes | A row is readable when ANY of these holds |\n\n#### Read scope by role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `member_of` | alias | yes | A role alias: whoever is in the group it binds to reads the row |\n\n#### Read scope by member\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A select_member field alias on this entity, or on the entity `through` lands on |\n| `is` | `"self"` | yes | The members this column names on a row read that row |\n\n#### Read scope by option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A single-select field alias on this entity, or on the entity `through` lands on |\n| `is` | list of alias (at least one) | yes | Its option aliases whose rows are readable \u2014 naming none would hide every row |\n\n<!-- generated:end entity -->\n\n<!-- generated:start rules-entity -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `entity.singular` | An entity a create is mounted over \u2014 a register that adds, a rows block \u2014 states its `singular`: one row of it, in the business\'s words. |\n| `entity.required-cycle` | Required links never wait on each other in a loop: no row of the loop could be created first. |\n| `entity.link-format` | A text field of `format: "link"` holds web addresses; one whose rows hold a phone or a mail address states no format. |\n| `entity.read-scope` | A `read_scope` clause names a declared role (`member_of`) or a field of its entity reached through one-row links: `"self"` a member field, options a single select declares. |\n| `entity.unique` | A `unique` set names two fields or more of its entity once each, each holding one value to compare \u2014 text, a number, a date, a single select, a one-row link \u2014 and is stated once; one field alone is `unique: true` on it. |\n\n<!-- generated:end rules-entity -->\n\n**`unique` is a set of values no two live rows share.** Each entry names fields\nof this entity holding ONE value \u2014 text, number, date, a single select, a link\nof cardinality `"one"` \u2014 and a create or update landing a second row with the\nsame values is refused. A set of one text field is that field\'s own `unique:\ntrue`, so it is refused here. A create carries each set in the names its panel\nsends, so the panel can name the duplicate before the write does.\n\n**`singular` is what a create says** \u2014 `New Order`, `Add Claim line`, `H\u1ED3 s\u01A1\nm\u1EDBi` \u2014 while `label` names the table, so without it the button reads `New\nOrders`. Nothing derives it: English plurals are irregular, and no language is\nexempt. In a language without plural forms it is usually the label itself,\nless any word for the collection. `model apply` REFUSES a model where a table\nsome create opens \u2014 a register\'s own, or a record section\'s add \u2014 states none.\n\n**`writes: false` says no app opens, edits or files these rows** \u2014 a log of what\nthe system sent, a copy of what another system holds. A record listing them\nkeeps the section and opens each row at rest, with no Add; a screen over the\nentity operates at most the rows its record owns; a party of it is picked, never\nfound or minted; and the generator writes no create and no update for it, and\nnever notes it as created nowhere. It shapes the apps, not who may write: a\ndirect write \u2014 the records API, the chat agent, an import \u2014 follows the table\'s\naccess, as on any table. A `lifecycle` on it is refused \u2014 a row walked\nthrough stages is worked by a person \u2014 and so is a publish desk over it (a rows\nblock\'s `post`), whose ticks make its rows. Absent,\napps open and edit the rows; `true` is not a value.\n\n**`read_scope` is a ROW rule, enforced by the platform.** It is resolved at\napply into the table\'s own row filters, so an app, a workflow reading for a\nviewer, and the API all answer the same rows \u2014 a per-record visibility field the\napp merely honours is a convention, not a gate. A `"self"` clause reads a column\nof one member or several, and every role alias and option alias a clause names\nmust be one this model declares. `apply` writes the rule onto a table it\nCREATES; a table it adopted that ALREADY CARRIES a rule keeps that one, because\nthe rule is the workspace\'s own statement about its rows \u2014 and a run whose model\nstates a different rule reports the entity rather than leaving the claim silent.\n\n**An app may state that its sharing is its read gate: `"reads": "shared"`.** An\napp reads as its owner, so the rule reaches a viewer only as the predicate every\nquery and editor guard of every app over the entity carries \u2014 right for a desk of\none\'s own rows, wrong for a desk whose audience its sharing already decides, where\nwidening the rule meant a role group nobody remembers to fill. Stated on an APP,\nits queries, pickers and guards carry no entity\'s rule and whoever the app is\nshared with reads and writes every row it draws; every other app and the table\'s\nown filters keep the rule. Share it deliberately. Refused on an app none of whose\ntables states a `read_scope`.\n\n**A row rule is never inherited.** A child read under a private record states\nits own: the record\'s `member_of` clauses as they are, and its column clauses\n`through` the one-row link the child hangs by \u2014 over one hop or several, each a\nlink of `"cardinality": "one"`, since over several rows the rule would admit a\nreader any one of them admits while the editor\'s guard reads the first. Only the\nstatus history takes its record\'s rule.\n\n## Field\n\nEvery field carries the keys below, and its `type`\'s section adds the rest; a\ntype whose section names no `default` takes none. `label` may not contain `{` or\n`}` (formulas reference fields by label at the platform level). Every row in `rows` states each\n`required` field it carries (neither a default nor a `default_from` copy is applied to them), and a required\nLINK is written with its row: the entity it names is created first, and entities\nwhose required links name each other are refused, since none of their rows could\never be created.\n\n**A stored field\'s `writes: false` says no person keys it in an app** \u2014 a post\'s\nid a publishing workflow brings back, a count a sync fills. Every app draws it\nread wherever it is placed, no add or act asks it, no paper fills it, and the\ngenerator writes it in no create and no update; a statement line reads it as a\nfigure no person keys, and beside `default_from` it is copied when the row is\nmade and never keyed. It shapes the apps, not who may write: a workflow, the\nrecords API, the chat agent and an import write it as the table\'s access allows.\nA formula, a rollup, a lookup, an autonumber and a date `derive_from` stamps take\nno such key \u2014 no one keys them already. Absent, a person keys the field; `true`\nis not a value.\n\n<!-- generated:start field -->\n\n#### Field\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"date"` \\| `"boolean"` \\| `"select"` \\| `"select_member"` \\| `"select_record_link"` \\| `"files"` \\| `"formula"` \\| `"rollup"` \\| `"lookup"` \\| `"autonumber"` | yes | What the field holds \u2014 each type takes the further keys its own section lists |\n| `alias` | alias | yes | Stable local alias, unique within the entity |\n| `label` | text | yes | The field\'s name in the workspace, which apply names it |\n| `description` | text | no | The field\'s description, written onto the field in the workspace |\n| `required` | boolean | no | Refuse a record whose cell for this field is empty. Apply writes it onto the field, and every write path \u2014 create, update, an agent\'s tool call, a workflow\'s set \u2014 refuses the row by field name. |\n\n<!-- generated:end field -->\n\n### `text`\n\n<!-- generated:start field-text -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `unique` | boolean | no | Unique values required |\n| `format` | `"text"` \\| `"link"` \\| `"markdown"` | no | How the words are drawn \u2014 plain, as a link that opens, or as markdown |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-text -->\n\n### `number`\n\n<!-- generated:start field-number -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | number | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. Omitted, a decimal. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-number -->\n\n`notation` is how the number reads, and every surface reads it:\n`{ "style": "currency", "currency": "USD" }` prints as money in its code,\n`{ "style": "unit", "unit": "kg" }` as a quantity with its unit after it,\n`{ "style": "unit", "unit": "percent" }` as a percent \u2014 10 is "10%", never\nmultiplied \u2014 and `{ "style": "decimal" }`, or no notation, as a plain number. One\nkey, so a figure reads one way.\n\nThe keys `notation` replaced \u2014 `format`, `currency`, `unit`, `unit_field` and\n`currency_field`, on a number or in a formula, and a formula\'s `format: "link"`\nfor `link: true` \u2014 are no longer read. A field stating them naming another\nreading than its `notation` (a decimal, where it states none) is refused, and the\nfinding names the `notation` to write in their place: `"format": "currency",\n"currency": "USD"` becomes `"notation": { "style": "currency", "currency": "USD" }`.\nWrite `notation`.\n\nAn ABSENT number is drawn absent \u2014 the one exception is a\n`sum` or count rollup the plan reads as a **`measure`**: that is the thing\naccumulated toward a bound, so nothing accumulated yet is zero and the meter\ndraws it. The same rollup read as an `amount` keeps its blank, and so does every\nother role: nothing added to what a row is WORTH means unpriced, not free. A\nformula reading only such sums and counts, and reading zero where each does\n(`{received} - {refunded}`), is one too. A `min`, an `avg`, a percentage of\nnothing and every other formula stay blank in any role, because none of them has\nan answer to give. This is why the pair on one\nscreen reads two ways \u2014 what has come in against what is owed \u2014 and why a\nmeasure\'s own LIMIT, an amount, leaves an unquoted row out of the count rather\nthan reporting it as nothing collected. **AND WHERE THAT LIMIT IS ABSENT \u2014 OR\nZERO \u2014 THERE IS NO LEVEL AT ALL**: a level is a reading AGAINST a bound, so a row\nthat states no bound, or a bound of nothing, draws nothing \u2014 cell, fact and all \u2014\nrather than a numerator whose whole meaning was the comparison. "Collected 0"\nbeside a blank total reads as money against a job worth nothing, and "0 of 0"\nagainst a count of nothing owed claims a comparison nobody can make. A measure the model gives no limit is a plain figure\nand is unaffected. A share is stored in percent units \u2014 68.1 is 68.1 % \u2014 and the\ncolumn, the fact behind it, the meter it is judged by and the figure over the\nregister all say so.\n\nA unit \u2014 `{ "style": "unit", "unit": "kg" }` \u2014 is what a figure counts or\nmeasures; `percent` aside, it is measured or counted. A **measured** unit is a code of one catalog \u2014 mass `g`\n`kg` `t`, volume `l` `m3` `cbm` (a cubic metre under freight\'s name), length\n`mm` `cm` `m` `km`, area `m2`, duration `min` `h` `day` \u2014 so a figure typed in\nanother unit of its dimension converts (`12,5 t` into a `kg` field is 12 500),\nand a tile or a chart reads it in the largest unit it reaches (12 500 kg as\n12,5 t\u1EA5n; CBM never scales) while a cell, a fact and a column always read in the\nfield\'s own. Any other noun of at most 12 characters (`ki\u1EC7n`, `pallet`, `TEU`) is a\n**counted** unit: a word after the figure that never converts. A measured unit is\nwritten as its code \u2014 `t\u1EA5n`, `KG` or `m\xB3` is refused, naming the code. A formula\nstates its own in `formula.notation`; a rollup that keeps the value (`sum`, `avg`,\n`median`, `min`, `max`, `range`) and a lookup carry the unit of the figure they\nread, exactly as they carry a currency, and a count carries none. A quantity is a\nnumber with its unit, never words (`3 cartons` in a text field): only a number\nsums, converts and reads down a column.\n\nA field\'s cells always hold figures in its own unit, so moving it between two\nunits of one dimension (`kg` to `t`) converts every stored figure \u2014 a change made\nin the workspace, in the field\'s settings (which say how many first) or through\n`lotics run update_table`. `model apply` never makes it: the model states the\nworkspace\'s unit until then. Any other change of unit\nrelabels.\n\nA figure whose unit or currency varies by row names a single select of its own\nrow as `per_row` \u2014 `{ "style": "unit", "unit": { "per_row": "uom" } }`,\n`{ "style": "currency", "currency": { "per_row": "currency" } }` \u2014 or a lookup of\none through a one-link, a line reading its shipment\'s currency. That select\'s\noptions are the vocabulary: each label a unit (`chi\u1EBFc`, `kg`), or an ISO 4217 code\n(`USD`). A label outside it is refused when the figure is written and when the\nselect is \u2014 its options, its type or its deletion while a figure names it. A row\nwhose select is empty reads its figure bare. Every app reads each row\'s figure in\nits own row\'s unit, and a sum never mixes units: totals are one per unit, a\nchart draws one at a time. A rollup that keeps the value (`sum`, `avg`, `min`\u2026)\nover such a figure stands only where the child\'s select is a lookup, through\nthe rollup\'s own link, of a select on this entity \u2014 the rollup reads that\nselect\'s unit; otherwise it is refused, as is any lookup of such a figure (its\nunit lives on the other row). A count is unaffected. Moving a field between a\nfixed and a per-row unit relabels.\n\n### `date`\n\n<!-- generated:start field-date -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. A date string in the field\'s format. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `format` | `"date"` \\| `"datetime"` \\| `"date_range"` \\| `"datetime_range"` | no | Whether the field holds a day or a moment, alone or as a span |\n| `timezone` | text | no | IANA timezone |\n| `derive_from` | `"created_at"` \\| `"updated_at"` | no | Auto-populate from the row\'s system timestamp; the field becomes read-only. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-date -->\n\nA `default` is refused beside `derive_from`: the platform stamps that date.\n\n### `boolean`\n\n<!-- generated:start field-boolean -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | boolean | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-boolean -->\n\n### `select`\n\n<!-- generated:start field-select -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | list of alias | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. Option alias(es) this field declares \u2014 one for single-select. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `options` | list of [Select option](#select-option) (at least one) | yes | The choices, in the order every picker and every ladder lists them |\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n#### Select option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable local alias, unique within the field |\n| `label` | text | yes | Display label for the option |\n| `color` | [colour](#select) | yes | The colour the option\'s badge is drawn in |\n| `mark` | [mark](#select) | no | The option\'s own mark, drawn in place of its colour dot wherever the option is shown: the brand it is ({kind: "brand", name: one of facebook, instagram, threads, meta, tiktok, google-ads, zalo, linkedin, x, google-meet, youtube, telegram, whatsapp, gmail, google-drive, outlook, kiotviet, misa, lark, payos}) a kit glyph ({kind: "icon", name: "wrench"}) or the face of who it is, drawn from its label ({kind: "avatar", name: "person" or "organization"}). Every option of a field has one, or none does. A mark a reader does not draw falls back to the dot |\n\n<!-- generated:end field-select -->\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\nEvery option is drawn in its colour wherever it shows \u2014 a cell, a row\'s line,\na record, a picker, the bar a reading splits the rows by.\n\nAn option may carry its own `mark`, drawn in place of its colour dot wherever\nthe option is shown \u2014 a stage, a chip, a filter, a fact, an entry of a log, a\nreading\'s part, a lookup of the select on another entity. Where the select is its\nrows\' `records` `mark`, a brand stands on the row\'s lead tile and any other option\nleads with its colour dot there. A select a reader scans\ndown a column \u2014 how a payment was made, the channel, the mode \u2014 states one on every\noption, since a glyph reads before its word: the brand it IS\n(`{"kind": "brand", "name": "tiktok"}` \u2014 one of `facebook`, `instagram`,\n`threads`, `meta`, `tiktok`, `google-ads`, `zalo`, `linkedin`, `x`,\n`google-meet`, `youtube`, `telegram`, `whatsapp`, `gmail`, `google-drive`,\n`outlook`, `kiotviet`, `misa`, `lark`, `payos`), a glyph the kit draws\n(`{"kind": "icon", "name": "wrench"}`; any other name is refused), or \u2014 where the\noptions name people or organisations who are no member and no row \u2014 the face of who\nit is, drawn from its label (`{"kind": "avatar", "name": "person"}` or\n`"organization"`). Every option of a select has one, or none does: a run of chips\nwhere one carries no mark reads as the one missing something. `apply` writes the\nmarks onto the table, sets one an adopted option lacks, and reports one it wears\ndifferently rather than overwrite it. The glyphs:\n\n<!-- generated:start field-select-glyphs -->\n\nactivity, align-center, align-left, align-right, align-vertical-justify-center, align-vertical-justify-end, align-vertical-justify-start, arrow-down, arrow-down-a-z, arrow-down-up, arrow-down-wide-narrow, arrow-left, arrow-left-from-line, arrow-right, arrow-right-from-line, arrow-right-left, arrow-up, arrow-up-down, arrow-up-wide-narrow, arrow-up-z-a, ban, banknote, bed, bell, between-horizontal-start, between-vertical-start, bold, bolt, book-marked, book-open, book-text, bot, box, brackets, brain, briefcase, building-2, calculator, calendar, calendar-clock, calendar-off, camera, car, chart-column, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down-up, chevrons-up-down, circle-alert, circle-check, circle-x, clipboard-list, clipboard-paste, clock, code, code-xml, columns-3, columns-3-cog, combine, construction, container, copy, credit-card, database, decimals-arrow-left, decimals-arrow-right, dollar-sign, download, ellipsis, eraser, expand, external-link, eye, eye-off, facebook, file, file-csv, file-down, file-question, file-spreadsheet, file-stack, file-text, file-up, folder, folder-closed, folder-open, folder-pen, form, funnel-plus, funnel-x, gauge, globe, gpu, grid-2x2, grip-vertical, group, hand-coins, hash, heading, heading-1, heading-2, heading-3, history, house, image, inbox, info, instagram, italic, keyboard, languages, layout-dashboard, layout-grid, library-big, link-2, link-2-off, linkedin, list, list-checks, list-collapse, list-filter, list-filter-plus, list-ordered, loader, lock, lock-keyhole, lock-keyhole-open, lock-open, log-in, log-out, mail, map-pin, maximize-2, megaphone, menu, message-circle, message-circle-question-mark, message-square, messages-square, mic, minimize-2, minus, monitor, mouse, mouse-pointer-click, music, newspaper, notepad-text-dashed, package, paint-bucket, paintbrush-vertical, palette, panel-left, panel-left-close, panel-left-open, panel-right, panel-right-close, panel-right-open, paperclip, pause, pencil, percent, phone, pin, pin-off, plane, play, plug, plus, receipt, rectangle-ellipsis, redo, refresh-cw, repeat, rotate-ccw, rotate-cw, rows-3, scan, scan-line, scissors, search, send, settings, share, share-2, shield, shield-alert, shield-check, shopping-cart, sigma, sliders-horizontal, smile, smile-plus, snowflake, sparkles, split, square, square-check, square-pen, square-sigma, stethoscope, sticky-note, strikethrough, table, table-2, tag, target, text-quote, text-wrap, thumbs-down, thumbs-up, ticket, trash, trending-down, trending-up, triangle-alert, truck, tv-minimal, twitter, type, underline, undo, upload, user, user-check, user-pen, users, utensils, waypoints, workflow, wrench, x, zap, zoom-in, zoom-out\n\n<!-- generated:end field-select-glyphs -->\n\n```jsonc\n"options": [\n { "alias": "short_video", "label": "Short video", "color": "zinc", "mark": { "kind": "brand", "name": "tiktok" } },\n { "alias": "print", "label": "Print", "color": "amber", "mark": { "kind": "icon", "name": "newspaper" } }\n]\n```\n\n### `select_member`\n\nA person picker over the workspace\'s members. No default: a model cannot name\nmembers of a workspace that does not exist yet. With no role it is still drawn \u2014\nface and name, ranked as a `party` \u2014 in a screen\'s `columns` and in the register\na record draws of these rows.\n\n<!-- generated:start field-select_member -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-select_member -->\n\n### `select_record_link`\n\n<!-- generated:start field-select_record_link -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `target_entity` | alias | yes | Alias of the entity this field links to |\n| `sync_both_ways` | boolean | no | Create a paired link field on the target entity for bidirectional sync |\n| `paired_field_alias` | alias | no | The pair edge of a bidirectional link: the field alias ON THE TARGET ENTITY that is this link\'s sync partner. Both sides of a pair carry it, each naming the other. Apply creates whichever side it reaches first WITH the pairing (the platform auto-creates the partner) and binds the partner alias to the auto-created field \u2014 without this edge the two contract fields would be created independently and collide with the auto-created partner. |\n| `cardinality` | `"one"` \\| `"many"` | no | How many linked records this field holds. Default \'many\'. \'one\' holds a single row and needs no partner; where the link IS paired, the partner side holds many. |\n| `display_field_aliases` | list of alias | no | Field aliases on the target entity shown as the link\'s display text / picker columns |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-select_record_link -->\n\nA two-way link is declared on BOTH sides, each naming the other as its\n`paired_field_alias`; the pair must be symmetric or the model is refused.\n\n**A single-valued link needs no partner.** `"cardinality": "one"` on its own is a\nlink that holds one row \u2014 one customer on an invoice, one project on a device \u2014\nand nothing is created on the target. The mirror invariant belongs to a PAIRED\nlink: pair a link when the target\'s own record should list what points at it, and\nleave it unpaired when it should not. Either way the record plan draws the\nrelation as a section on the side it points at, so an unpaired link costs the\ntarget nothing.\n\n### `files`\n\nA row attaches documents to it (\xA7 Rows). Each file\nis drawn by its kind, with no key to choose: a picture (an image, a video) as its\nthumbnail, a document (a PDF, a sheet) as its type\'s badge and its filename.\n\n<!-- generated:start field-files -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n\n<!-- generated:end field-files -->\n\n### `formula`\n\n<!-- generated:start field-formula -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | [Formula](#formula) | yes | Formula config. The expression references other fields on the SAME entity by alias in braces, e.g. `{quantity} * {unit_price}`. |\n\n#### Formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `expression` | text | yes | The expression, over fields of THIS entity by alias in braces \u2014 `{quantity} * {unit_price}` |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. For a formula yielding a number. |\n| `link` | `true` | no | The formula yields a URL as text, drawn as a link that opens. |\n| `options` | list of [Select option](#select-option) (at least one) | no | The categories the formula yields, drawn as a single select\'s options are (read-only). The expression yields one of them as `{this_field:option}`, or null \u2014 `{days_idle} > 30 ? {warmth:cold} : {warmth:hot}`. Omit for a formula yielding a plain value. |\n| `output_type` | `"number"` \\| `"text"` \\| `"date"` \\| `"datetime"` \\| `"boolean"` \\| `"select"` | no | What the expression YIELDS \u2014 the kind the platform infers at write time, declared here so the offline checks can read it. `notation` beside it is how a number result reads, not what it is. Ignored on the wire (the platform re-infers it); `lotics model pull` writes the inferred value. |\n\n<!-- generated:end field-formula -->\n\n`output_type` is what lets a role or a screen clause accept a computed value: a\ncaption over a derived name (`output_type: "text"`), a period over a settled date\n(`"date"`). A formula declaring no `output_type`, `notation` or `link` says nothing\nabout its result, and every rule that needs one refuses it by name. A formula stating\n`options` is a computed category: read-only, and read as a single select\nwherever it is drawn \u2014 a column, a filter, a reading\'s `by` or `where`.\n\n**A select reaches a formula as the KEYS of its chosen options**, a list \u2014\nnever their labels, and never their aliases \u2014 and a model has no keys: the\nworkspace mints them when the table is made. So an option is named in a formula\nas `{field:option}`, both aliases, and the copy writes that option\'s key in its\nplace: `includes({kind}, {kind:crate})` for a select holding one or several,\n`{kind}[0] == {kind:crate}` for a single one. A select compared to its own words\n(`{kind} != "Crate"`) matches no row and computes the other branch everywhere,\nso the check refuses it and names the token. A select looked up from another\nentity is tested there, in a formula of its own, and that result looked up.\n\n**The language.** The offline check and the platform compute a formula with one\nengine, and the check refuses a call or a name it cannot run, naming what to write:\n\n<!-- generated:start field-formula-language -->\n\n- Operators: `+ - * / %`, `== != > < >= <=`, `&& || !`; `+` also joins text\n- Conditionals: a ternary only \u2014 `{amount} > 100 ? "High" : "Low"`\n- Not supported: optional chaining (`?.`), nullish coalescing (`??`), template literals, arrow functions \u2014 use `get(obj, "path", default)`, `coalesce(v1, v2)`\n- Helpers are these names, spelled exactly; a spreadsheet function (`IF`, `SUM`, `LEN`, `DATEDIF`) is none of them, and a formula calling one is refused\n- Math: round(n,decimals?), ceil(n), floor(n), abs(n), min(a,b), max(a,b), sum(arr), mean(arr), clamp(n,min,max), percentage(part,total,decimals?), pow(base,exp), sqrt(n), mod(n,divisor)\n- Strings: upper(s), lower(s), trim(s), capitalize(s), length(s), contains(s,search), join(arr,sep), split(s,sep), replace(s,search,rep), replaceAll(s,search,rep), startsWith(s,prefix), endsWith(s,suffix), substring(s,start,end?), padStart(s,len,char), padEnd(s,len,char), numberToWords(n, lang?) (lang \'vi\' default, or \'en\')\n- Lists: includes(list,value), first(list), last(list), unique(list), compact(list) \u2014 length(list) counts one\n- Dates: now(), formatDate(d,fmt), addDays(d,n), subDays(d,n), addHours(d,n), subHours(d,n), addMinutes(d,n), subMinutes(d,n), startOfDay(d), endOfDay(d), differenceInCalendarDays(later,earlier), differenceInHours(later,earlier), differenceInMinutes(later,earlier), isBefore(d1,d2), isAfter(d1,d2), isSameDay(d1,d2), isToday(d), isWithinRange(d,start,end), parseDate(d)\n- Null/type: isNull(v), isEmpty(v) (also true for "" and []), coalesce(v1,v2,...), isString(v), isNumber(v), isBoolean(v), isArray(v), toNumber(v), toString(v)\n- Other: formatCurrency(amount,locale,currency), formatDecimal(value,decimals,locale) (grouped quantity, no symbol), get(obj,"path",default?)\n- Empty cells: a cell nobody filled is null inside a formula, whatever its type; one holding 0, false or "0" is not empty. Test it with `isEmpty({note})` \u2014 `{note} == ""` and `{done} == false` are false on an unset cell\n- Arithmetic over an empty cell: `+` and `-` read it as 0 beside a value (`{fee} + {surcharge}` is `{fee}` when the surcharge is empty, null when both are); `*`, `/`, `%` and a unary `-` yield null (`{price} * {qty}` is null, not 0, when the quantity is empty)\n- No helper throws on an empty cell: the math helpers and toNumber return null, the string helpers "". A value of the wrong type still errors. When EVERY field a formula reads is empty it is null \u2014 unless it reads each only as the argument of isEmpty, isNull or isNotNull (`!isEmpty({file})` is false there, not null)\n\n<!-- generated:end field-formula-language -->\n\n### `rollup`\n\nAggregates the records reached through a link on this entity.\n\n<!-- generated:start field-rollup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to roll up from |\n| `aggregate_option` | [aggregation](#rollup) | yes | Aggregation operation. Its `field_key` names a field alias on the linked entity. |\n| `filter` | [filter](#views) group | no | Only linked records matching this filter are aggregated; one condition on its own is a group of one. Every `field_key` in it names a field alias on the linked entity, and a select condition\'s value names an option alias there. A traversal node reaches past that entity, so its `path` hops and inner `field_key` are fully-qualified `entity.field` aliases. |\n\n<!-- generated:end field-rollup -->\n\n`aggregate_option` is `{ "operation": \u2026, "field_key": \u2026 }` \u2014 `field_key` a field\nalias on the linked entity (`count` may omit it), and `operation` one of `count`,\n`sum`, `avg`, `median`, `min`, `max`, `range`, `empty`, `filled`,\n`percent_empty`, `percent_filled`, `unique`, `percent_unique`, `earliest`,\n`latest`, `date_range`, `checked`, `unchecked`, `percent_checked`,\n`percent_unchecked`. The operation must be one the aggregated field\'s type\nallows \u2014 `sum` over a number, `earliest` over a date, `filled` over any stored or\nformula field. A lookup is never rolled up: roll up the child\'s own field, or a formula\nover it. A rollup states no `notation`: `sum`, `avg`, `median`, `min`, `max` and `range`\nread in the aggregated figure\'s, a count as a plain number, and a `percent_*` as a\npercent (0\u2013100), in the app as in the table.\n\n### `lookup`\n\nDisplays a field from the linked records.\n\n<!-- generated:start field-lookup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to look up through |\n| `lookup_field_alias` | alias | yes | Alias of the field on the linked entity to display |\n| `order_by` | [Lookup order](#lookup-order) | no | Show ONE linked row\'s value \u2014 the first in this order \u2014 rather than every linked row\'s |\n\n#### Lookup order\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_key` | text | yes | A field alias on the linked entity the rows are ordered by |\n| `direction` | `"asc"` \\| `"desc"` | yes | Which end of that order the one row is taken from |\n\n<!-- generated:end field-lookup -->\n\nInside a formula, a lookup holding one value is that value (`{due_soon}` is\n`true`, not `[true]`); several values are a list.\n\n### `autonumber`\n\n<!-- generated:start field-autonumber -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `prefix` | text | no | Literal prefix prepended to every display value (e.g. \'KH-\' \u2192 \'KH-001\'). Ignored when `template` is set. |\n| `padding` | integer | no | Zero-pad the integer to this width. Default 1 (no padding). 3 \u2192 \'001\', \'012\', \'123\', \'1234\' (overflow uses the actual width). Ignored when `template` is set. |\n| `template` | text | no | Format template with placeholder tokens evaluated at insert time. Tokens: {N} (raw integer), {N:W} (zero-padded to width W, e.g. {N:3} \u2192 001), {YEAR} (4-digit year), {YEAR:2} (2-digit year), {MONTH} (2-digit month), {DAY} (2-digit day). Date tokens use the workspace timezone. Example: \'HM-{YEAR}-{N:3}\' yields \'HM-2026-001\'. Stored as the composed string; subsequent template edits do NOT re-format existing rows (date tokens would lose the original creation date). |\n\n<!-- generated:end field-autonumber -->\n\n<!-- generated:start rules-field -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `field.default` | A default names options its select declares \u2014 one on a single select \u2014 and a date stamped by `derive_from` states none. |\n| `field.default-from` | A field\'s `default_from` reads `<link>.<field>` off the row a one-row link of its entity names \u2014 a link no copy fills \u2014 into a field of the same type, date format and notation that `derive_from` does not stamp: a lookup only through a one-row link, a link only into one to the same entity, a select\'s option as this field\'s of the same alias (one it lacks is not copied), several values only into a field holding several. The server copies it on every add that fills the link and leaves the field empty, so an add sure to fill the link \u2014 a required one, or the record it files the row under \u2014 does not ask it by default. |\n| `field.writes` | A stored field\'s `writes: false` says no person keys it in an app \u2014 a workflow or an integration writes it: every app draws it read, no add (`create`) or act asks it and no paper fills it, and a required one takes a `default` or a `default_from` where an app adds its rows; never on a date `derive_from` stamps, which is read already. |\n| `field.option-mark` | An option\'s `mark` is one the kit draws, and a select\'s options mark every one or none. |\n| `field.unit-select` | A notation\'s `per_row` names a single select of the same row, or a lookup of one through a one-link, whose every option label is a unit, or an ISO 4217 code. |\n| `field.link` | A link targets a declared entity: its `display_field_alias` a field of it, its `paired_field_alias` a link on it naming this one back \u2014 and of two paired links at most one reads one row. |\n| `field.formula` | A formula parses, reads fields of its own entity by alias, names an option as `{field:option}` of one a select declares, and compares a select to its options\' keys, never their words. |\n| `field.formula-type` | A formula yields what it states \u2014 its `output_type`, else a figure where it states a `notation` and text where it states `link` \u2014 as the platform infers it from the expression when the field is created. |\n| `field.rollup` | A rollup aggregates, through a link of its entity, a field of the linked entity by an operation a model declares and that field\'s type takes \u2014 never a figure read in each row\'s own unit or currency \u2014 filtered on fields of the linked entity. |\n| `field.lookup` | A lookup reads, through a link of its entity, a field of the linked entity \u2014 never one read in each row\'s own unit or currency \u2014 ordered by a field of it. |\n| `field.computed-cycle` | Computed fields never wait on each other in a loop: each is computed after what it reads. |\n\n<!-- generated:end rules-field -->\n\n## Views\n\nSaved views live under the entity they belong to. Every field reference is a\nfield ALIAS on that entity.\n\n<!-- generated:start views -->\n\n#### View\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable view alias, unique within the entity |\n| `label` | text | yes | Display name of the view |\n| `description` | text | no | The view\'s description, written onto the view in the workspace |\n| `columns` | list of [View column](#view-column) (at least one) | no | The columns the view shows, in this order and no others; absent, every field |\n| `filters` | [filter](#views) | no | The rows the view keeps |\n| `sort` | [sort](#views) | no | The order the view reads its rows in |\n| `summary` | map of text \u2192 text | no | Field alias \u2192 the operation its footer cell states |\n| `frozen_columns` | integer \\| `null` | no | How many leading columns stay in place while the rest scroll |\n\n#### View column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_alias` | alias | yes | A field of this entity |\n| `visibility` | `"visible"` \\| `"hidden"` | yes | Field visibility state: \'visible\' = shown to everyone, \'hidden\' = not shown by default but members can toggle |\n| `width` | number | no | The column\'s width, in pixels |\n\n<!-- generated:end views -->\n\nA filter is a group \u2014 `{ "node_type": "group", "logic": "and" | "or",\n"children": [ \u2026 ] }` \u2014 or one condition on its own, `{ "node_type":\n"condition", "type": "select", "field_key": "tier", "operator": "has_any_of",\n"value": ["gold"] }`. A sort is a list of `{ "field_key": \u2026, "order": "asc" |\n"desc" | null }`. A condition\'s `type` is the field\'s type and its `operator` is\none that type admits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n<!-- generated:start rules-view -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `view.fields` | A view\'s columns, summary, sort and filters name fields of its entity. |\n\n<!-- generated:end rules-view -->\n\n## Roles\n\nA role becomes a workspace group.\n\n<!-- generated:start roles -->\n\n#### Role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable role alias, bound to a workspace group when the model is applied |\n| `label` | text | yes | The group\'s name in the workspace |\n\n<!-- generated:end roles -->\n\n## Templates\n\nAn `html` or `email` template carries its content inline, and `{{name}}` in it is filled\nfrom the workflow\'s data; an `excel` template names an uploaded workbook by its `file_id`,\nfilled with the register\'s report by its `export` alone.\n\nInline content is refused where it holds a script, a frame, a `<link>`, an inline event handler or a\n`javascript:` URL; an `html` one may be a fragment, kept as written and printed as a whole document. An apply brings a\ntemplate it made before to the content the model states, and leaves one it adopted by its label as\nits owner wrote it.\n\n<!-- generated:start templates -->\n\n#### Template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"html"` \\| `"email"` \\| `"excel"` | yes | html is a page a workflow renders to a PDF; email is a message a workflow sends; excel is a workbook a register\'s export fills |\n| `alias` | alias | yes | Stable template alias, unique within the contract |\n| `label` | text | yes | The template\'s name in the workspace |\n\n<!-- generated:end templates -->\n\n### `html` and `email`\n\n<!-- generated:start template-inline -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `content` | text | yes | Inline template content; placeholders reference field aliases |\n| `content_sha256` | text | no | sha256 (64-char lowercase hex) of the utf-8 content; derived where the template is written when absent |\n| `locale` | `"vi"` \\| `"en"` | no | The language its figures, dates and amounts in words print in; absent prints in the organization\'s |\n\n<!-- generated:end template-inline -->\n\n### `excel`\n\n<!-- generated:start template-excel -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `file_id` | text | yes | The uploaded .xlsx (`fil_\u2026`) the template is made from; its markers read what the export fills it with |\n\n<!-- generated:end template-excel -->\n\nA paper that has to look like a counterparty produced it \u2014 an official letter,\nan acceptance minute, a supplier\'s bill \u2014 is the same `html` template with a\nshell around the body: a letterhead, a reference line, a seal and a signature\nblock, and paper grain over everything. One shell, many bodies; the data is the\nonly thing that changes, so a workflow can re-issue it over any record.\n\n```jsonc\n{ "alias": "cong_van", "label": "C\xF4ng v\u0103n", "type": "html",\n "content": "\u2026the page below, as one JSON string\u2026" }\n```\n\n```html\n<style>\n .sheet{position:relative;width:718px;padding:44px 58px 30px;background:#fbfaf6;color:#111;font:14.2px/1.5 \'Liberation Serif\',serif}\n .grain{position:absolute;inset:0;opacity:.34;mix-blend-mode:multiply;background:url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'140\' height=\'140\'><filter id=\'f\'><feTurbulence baseFrequency=\'.9\' numOctaves=\'2\'/><feColorMatrix values=\'0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .35 0\'/></filter><rect width=\'140\' height=\'140\' filter=\'url(%23f)\'/></svg>")}\n .top{display:flex;text-align:center;font-size:13.4px} .top>div{flex:1} .u{display:inline-block;border-bottom:1px solid #111;font-weight:700}\n .ref{display:flex;text-align:center;font-size:13.4px;margin-top:6px} .ref>div{flex:1} .ref .r{font-style:italic}\n h1{text-align:center;font-size:15.6px;margin:26px 0 18px} p{text-align:justify;text-indent:26px;margin:0 0 9px}\n .sig{display:flex;margin-top:20px} .sig .l{flex:1} .sig .r{width:290px;text-align:center;position:relative}\n .sig .nm{font-weight:700;margin-top:96px} .seal{position:absolute;left:4px;top:8px;width:166px;height:166px;opacity:.66;mix-blend-mode:multiply;transform:rotate(-17deg)}\n </style>\n <div class=\'sheet\'><div class=\'grain\'></div>\n <div class=\'top\'><div><b>{{issuer_parent}}</b><br><span class=\'u\'>{{issuer}}</span></div>\n <div><b>C\u1ED8NG H\xD2A X\xC3 H\u1ED8I CH\u1EE6 NGH\u0128A VI\u1EC6T NAM</b><br><span class=\'u\'>\u0110\u1ED9c l\u1EADp - T\u1EF1 do - H\u1EA1nh ph\xFAc</span></div></div>\n <div class=\'ref\'><div>S\u1ED1: {{number}}</div><div class=\'r\'>{{place}}, ng\xE0y {{day}} th\xE1ng {{month}} n\u0103m {{year}}</div></div>\n <h1>{{title}}</h1>\n <p>K\xEDnh g\u1EEDi: {{recipient}}.</p>\n {{{body}}}\n <div class=\'sig\'><div class=\'l\'><b>N\u01A1i nh\u1EADn:</b><br>- Nh\u01B0 tr\xEAn;<br>- L\u01B0u VT.</div>\n <div class=\'r\'><img class=\'seal\' src=\'{{seal_url}}\'><b>{{signer_title}}</b><div class=\'nm\'>{{signer}}</div></div></div>\n </div>\n```\n\n**What an act\'s template is handed** is its record\'s own fields by alias: a\nselect, a member and a link as their words (a link as the linked row\'s title), a\nnumber, a date and a computed value that declares its kind raw (`2800000`,\n`2026-09-14`) \u2014 never a files field or a computed select. On one record it is\nhanded, too, the rows of each child the record draws as `rows` whose alias the\ntemplate names, listed under that alias for `{{#each <alias>}}` in place of the\nrecord\'s own link to them: every row filed under the record, oldest first, each\nby its own fields as the record\'s are, less its link back to the record. Over\nseveral rows the rows are listed under the entity\'s alias, for\n`{{#each <alias>}}`, without their child rows.\n\nHelpers print a raw value in the template\'s `locale` \u2014 `vi` or `en`, the organization\'s language where it states\nnone; each prints `""` for an empty value:\n\n| Helper | Prints (`vi` / `en`) |\n|---|---|\n| `{{money total "USD"}}` | `1.250,50 US$` / `$1,250.50`, in the currency\'s minor units; no code, a number |\n| `{{number qty}}` | `1.234,5` / `1,234.5`: grouped, up to 2 decimals |\n| `{{date issued_on}}` | `14/09/2026`; a date-fns pattern as the second argument, `{{date at "dd/MM/yyyy HH:mm"}}`, literal text quoted (`"\'Ng\xE0y\' dd"`). A moment prints on the workspace\'s timezone |\n| `{{words total "USD"}}` | `M\u1ED9t ngh\xECn hai tr\u0103m n\u0103m m\u01B0\u01A1i \u0111\xF4 la M\u1EF9 n\u0103m m\u01B0\u01A1i xu` / `One thousand two hundred fifty US dollars and fifty cents`; no code, VND (`\u0111\u1ED3ng`) |\n\nA value it cannot read (`{{money "abc"}}`, `{{date "14/09/2026"}}`) fails the render. A field named like a\nhelper and written alone (`{{number}}`) prints its own value.\n\n## Rows\n\nFirst records, keyed by entity alias. Up to 200 rows per entity and 2000 across\nthe model, attaching at most 2000 documents between them \u2014 a real data set\nbelongs in an import, not a model.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n<!-- generated:start rows -->\n\n#### Row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | text | yes | Local handle for this row, referenced by other rows\' link fields |\n| `fields` | map of text \u2192 any value | yes | Field alias \u2192 the value, read against the field\'s declared type |\n\n<!-- generated:end rows -->\n\n<!-- generated:start rules-rows -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `rows.ref` | A row\'s `ref` is unique within its entity. |\n| `rows.required` | A row states every field its entity requires: it is created with what it states, its fields\' defaults and copies unapplied. |\n| `rows.distinct` | Open rows of an entity a register lists read apart: no two state the same title and the same line under it, and an autonumber on the line reads every row apart. |\n| `rows.value` | A row\'s value fits its field: an option alias for a select, `"<entity>:<ref>"` of a row of the linked entity in this file for a link, `"self"` for a member, a relative path beside the file or a `fil_` id for files, a date in the date grammar (an hour only on a datetime), a text, number, yes/no or null otherwise \u2014 and none on a computed field. |\n\n<!-- generated:end rules-rows -->\n\nA `ref` is lowercase letters, digits and underscores, and is never persisted.\n\nA `files` cell attaches documents: paths relative to this file (no `..`, never\nabsolute), which the check proves exist and `apply` uploads into the workspace\nbefore any row is written \u2014 a paperwork business seeds its papers with its\nrows. The server accepts only `fil_` ids of files this workspace owns, which is\nwhat the upload leaves behind. After a run that wrote rows, the WORKSPACE holds\nwhich record each row became, under the row\'s own `<entity>:<ref>`:\n`delete_records` over them is how a seeded set is reset, and applying again\nre-dates it.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `select_record_link` | `"<entity-alias>:<ref>"` naming another row in this file \u2014 `"customer:acme"`, or an array for several; a paired link is stated on ONE side (the child\'s link to its parent) and its partner fills itself |\n| `select_member` | `"self"` only \u2014 the person applying the model |\n| `files` | paths beside this file \u2014 `["scans/pccc_letter.png"]` \u2014 uploaded by `model apply`/`setup` before the model is sent; or `fil_` ids of files already in this workspace |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe model is applied, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n- on a date that holds its hour (`format: "datetime"`), that hour after the day, and always stated:\n `@today 14:30`, `@today+1 06:00`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Records\n\n`records` says how a row of each entity is RECOGNISED \u2014 once per entity, keyed by\nentity alias \u2014 and every surface that draws one of its rows reads the same\nstatement: the register\'s row, a picker\'s option, a link\'s chip, a child table\'s\nrow and the record\'s header. Every entity an app lists, opens or picks has one.\n\n```jsonc\n"records": {\n "visit": {\n "title": "container_no",\n "subtitle": ["customer", "arrived_on"],\n "image": "photos",\n "status": { "field": "stage", "closed": ["gone"], "history": "visit_history" },\n "figure": "total_fees",\n "due": ["free_until"]\n },\n // A release order read against what it allows: a meter wherever it shows.\n "release": { "title": "release_no", "figure": "issued", "limits": { "issued": "units" } },\n // A dossier\'s stage is the last date it reached \u2014 no stored select.\n "dossier": { "title": "applicant", "status": { "milestones": ["received_on", { "field": "appraised_on", "when": { "kind": ["loan"] } }, "signed_on"], "closed": ["signed_on"] } },\n // A site owes a diary page each day it works, from breaking ground through handover.\n "site": { "title": "name", "cadence": [{ "entity": "diary_page", "on": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "started", "until": "handed_over" }] }\n}\n```\n\n<!-- generated:start records -->\n\n#### Record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | alias | yes | The field that names a row \u2014 the first thing every surface shows: who or what it is, a link naming it by the linked row (a job read by its customer). An autonumber is refused; a typed number is the subtitle |\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title wherever a row is drawn (a code, a date, a party): one line at a desk, wrapping to a second on a phone |\n| `image` | alias | no | A files field that pictures the row \u2014 never one an act keeps the document it makes in |\n| `mark` | alias | no | A single select whose option leads the row wherever a mark leads it \u2014 a list\'s row, the record\'s head, a link naming the row: the option\'s brand, else its colour dot, on a tile. The field stays a field: a subtitle run, a column, a section\'s control. Never beside `image` or `party`, and never the status field |\n| `description` | alias | no | A text field saying what the row is about \u2014 a plan\'s brief, a customer\'s note: read in its record\'s head under the title\'s line, its first lines at rest and whole on a press, and written from the head\'s \u270E. Never on a row\'s line |\n| `party` | `"person"` \\| `"organization"` | no | What each row is where the rows are people (a contact, a patient) or organisations (a customer, a supplier, a carrier); absent, the rows are things (an order, an item, a paper) |\n| `status` | [Status](#status) | no | The stage a row moves through \u2014 a single select or its milestone dates \u2014 drawn as a badge beside its title |\n| `figure` | alias | no | The one number the row stands for, read at its right (a total, a quantity left) |\n| `limits` | map of alias \u2192 (alias \\| number) | no | A number read against a bound \u2014 a field of the same row or a constant \u2014 drawn as a meter wherever it shows |\n| `gates` | map of alias \u2192 (alias \\| number) | no | A pass mark on a meter `limits` bounds \u2014 a field of the same row or a constant; a percent field is its share of the bound, any other number an amount. The meter marks it and fills complete once past it |\n| `tolerance` | map of alias \u2192 number | no | How far past its `limits` bound a meter may stand, in percent of the bound: the bound is an amount expected (received against ordered), not a cap. Without one the bound is a cap, and past it is an overrun |\n| `due` | list of alias (at least one) | no | Dates that are deadlines (stored, or computed as a date): each counts down and turns overdue |\n| `frees` | alias | no | A date ending the row\'s span of days on which what the row holds is free again (a check-out, a hire\'s return): a stay from the 3rd to the 5th holds the 3rd and the 4th, and another may start on the 5th \u2014 on a lanes board, a calendar, a roster and in `no_overlap` alike. Absent, a span of days holds its last day; a span of moments always frees its end |\n| `settled` | alias | no | A yes/no of the row (stored, or a formula: an invoice paid in full) that ends its deadlines: while it is true, no `due` of the row counts down or turns overdue \u2014 in a cell, on its line, in its head, on a calendar or gantt, or in the overdue count. A dashboard\'s or reading\'s `where` reads it as any other yes/no |\n| `task` | `true` | no | A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record\'s rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed` |\n| `cadence` | list of [Cadence](#cadence) (at least one) | no | The rows of a child each row owes one of a day, on the days of the week listed (a site\'s diary page, Monday to Saturday) \u2014 one entry a child, never owed on a day still to come. A roster of these rows over days marks a past day owed with none missing in the danger tone, counted in the legend; today\'s is due, offered to add and never marked; each row\'s total reads the days it holds of those owed |\n| `applies` | map of alias \u2192 map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Fields that apply only while their conditions hold (a length only on a line whose tariff charges by the metre): where one does not apply no add asks it and no surface shows it. Never the title; a required one starts at its `default` |\n| `starts` | map of alias \u2192 `"today"` \\| `"me"` | no | What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default` |\n| `scan` | `"text"` \\| `"iso6346"` | no | Its title, a text field, is painted or printed on the thing a row is (a container\'s number, a seal\'s) and read off one photo on a phone, beside each search matching it and each field holding or picking it: `iso6346` a container number, refused unless its check digit holds; `text` as it stands |\n| `place` | [Place](#place) | no | Where a row stands, as plain fields of it, drawn as one place where its record draws the address: how sure it is, a press opening it in the device\'s maps, and one taking it from where the device stands |\n| `on_site` | [On site](#on-site) | no | Photos taken where a place stands (a site diary\'s): a photo taken through a phone\'s camera here keeps where the device stood, marked at the place, away from it or unknown, and a record holding one taken away says so among its checks \u2014 one with no place (picked on a computer, or kept before places were) is marked and warns nothing; a place reaches only members of the organization, through the apps drawing these photos |\n| `pin` | [Pin](#pin) | no | Each row is a place on a picture (a spot on a container\'s drawing, a room on a plan): a record listing rows that link to these by a single link draws that picture over them, a numbered pin at each listed row\'s place, each row led by its number, the row opened lit on it and a pin pressed opening its row |\n\n#### Cadence\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | The child entity owed: each of its rows stands on one row of this entity by its single link to it, and on the first date of its line (`records` title or subtitle) |\n| `on` | list of `"mon"` \\| `"tue"` \\| `"wed"` \\| `"thu"` \\| `"fri"` \\| `"sat"` \\| `"sun"` (at least one) | yes | The days of the week one is owed on, each listed \u2014 `mon` to `sun` |\n| `from` | alias | no | A date of this row: the first day one is owed (a site breaking ground, a contract\'s start); absent, no first day |\n| `until` | alias | no | A date of this row: the last day one is owed (a site\'s handover, a contract\'s end); empty or absent, through today |\n\n#### Place\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `address` | alias | yes | A text field: the address, as people write it |\n| `latitude` | alias | yes | A number field: degrees north, negative south |\n| `longitude` | alias | yes | A number field: degrees east, negative west |\n| `accuracy` | alias | no | A number field: how many metres off the device that gave the place said it may be |\n\n#### On site\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photos` | list of alias (at least one) | yes | Files fields of this row holding photos taken on site |\n| `place` | alias | no | A single link of this row to the row whose `place` its photos are taken at (a diary page\'s site); absent, this row\'s own `place` |\n| `radius` | number | yes | How many metres from the place a photo still counts as taken there |\n\n#### Pin\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `on` | alias | yes | A single link of this row to the row whose picture (its `records` `image`) the place is on \u2014 a container\'s drawing, a floor\'s plan |\n| `x` | alias | yes | A number field of this row: how far across that picture the place stands, in percent from its left edge |\n| `y` | alias | yes | A number field of this row: how far down that picture the place stands, in percent from its top edge |\n\n<!-- generated:end records -->\n\n<!-- generated:start rules-records -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.title` | A title names the row by what it is \u2014 never an autonumber; a code is its subtitle, and the subtitle never names the title\'s own field. |\n| `records.image` | `image` is a files field, never one an act keeps the documents it makes in (`into`). |\n| `records.mark` | `mark` is a single select, never the status field, and never beside `image` or `party` \u2014 a list keeps one lead slot \u2014 nor on the folder an app works inside (`scope`), which its switcher names by a picture or its initials. |\n| `records.description` | `description` is a text field, read in the record\'s head \u2014 never a section\'s too. |\n| `records.figure` | A figure is a number. |\n| `records.applies` | `applies` holds a field of the row other than its title to conditions of the same row \u2014 a yes/no (stored or a formula) by `true` or `false`, a single select by its options, its own or looked up through a one-row link \u2014 never to itself; a required field it holds starts at a `default`. |\n| `records.limits` | `limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit. |\n| `records.gates` | `gates` marks a pass on a meter `limits` bounds: a percent field (its share of the bound), another number in the bound\'s unit, or a constant. |\n| `records.tolerance` | `tolerance` stands on a meter `limits` bounds. |\n| `records.starts` | `starts` starts a field a person writes \u2014 `today` a date or a moment (never a span), `me` a member \u2014 and an add that does not ask a field it starts writes that start. |\n| `records.due` | A deadline (`due`) is a date. |\n| `records.frees` | `frees` names a stored day, never a moment: a span of moments already frees its end. |\n| `records.settled` | `settled` names a yes/no of the row (stored, or a formula) beside a `due`: while it holds, the row\'s deadlines neither count down nor turn overdue. |\n| `records.task` | A task entity (`task`) states a `status` of a stored single select with `closed` options \u2014 its ring ticks a row done by moving it to one. |\n| `records.cadence` | A `cadence` names each child once \u2014 its rows standing on one row of this entity by their single link and on the first date of their line \u2014 lists each day of the week once, and `from` and `until` are two different days of this entity. |\n| `records.scan` | A title read off a photo (`scan`) is a text field the row stores. |\n| `records.place` | A `place` names four different fields of the row: its address a text field, its latitude, longitude and accuracy number fields it stores. |\n| `records.on_site` | `on_site` lists files fields of the row, each once, taken at the `place` of the row itself or of the one row a single link of it names (`place`), which states one. |\n| `records.pin` | A `pin` names a single link of the row (`on`) to an entity whose `records` states its `image`, and two different number fields of the row, how far across (`x`) and down (`y`) that picture in percent. |\n| `records.picture` | *Refused until adopted or declined:* Rows an app draws that each hold one files field no act makes, no call is filed into, no files block keeps and no list compares as a pair (`compare`) \u2014 and no figure \u2014 are pictured by it (`image`); where the workspace\'s files are known, the field holding images is the picture, a figure or not. |\n\n<!-- generated:end rules-records -->\n\n- **The title names the row** by who or what it is \u2014 a link by the linked row (a job\n read by its customer). An autonumber is refused as a title; a typed number is the\n subtitle.\n- **One lead per row.** A row leads with its picture (`image`), a face (`party`) or the\n option of a single select it holds (`mark`) \u2014 never two. A `mark` draws that option\n on a tile: the brand it is, else its colour dot; a row holding none, a ring saying\n none is picked. The same tile heads the row\'s record and leads it where a link\n names it; the select stays a field \u2014 a subtitle run, a column, a section\'s control.\n- **A limit** reads a number against a bound \u2014 another field of the row, or a constant.\n A `gates` entry marks a pass on that meter: a percent field is its share of the\n bound, any other number an amount.\n- **A deadline** (`due`) counts down wherever it is drawn and is overdue once past while\n the work is open. A register over the entity says how many rows in view are overdue on\n its summary line, and pressing that count narrows to them \u2014 no reading to state.\n- **A span\'s free day** (`frees`) is the end date on which what the row held is free\n again \u2014 a check-out, a hire\'s return. A stay from the 3rd to the 5th holds two nights:\n a lanes board, a calendar and a roster draw it through the 4th, and `no_overlap` lets\n the next stay start on the 5th. Without it a span of days holds its last day; a span of\n moments always ends as the next may begin.\n- **`starts`** is where an add\'s field begins, stated only where that is certain \u2014 a\n log\'s own day at `today`, a request\'s requester at `me`. Nothing else starts a field\n but its declared `default` and what the reader narrowed the register to.\n\n## Status\n\nA record\'s `status` is the stage a row moves through \u2014 a stored single select, or the\nmilestone dates it reaches \u2014 drawn as a badge beside its title wherever the row is.\n\n<!-- generated:start status -->\n\n#### Status\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | no | The single select that is the row\'s state \u2014 or `milestones` instead |\n| `milestones` | list of (alias \\| [Milestone](#milestone)) (at least 2) | no | Dates in the order the work reaches them, instead of a stored select: the stage is the last one filled, ticking one stamps today, and clearing one clears every later one |\n| `closed` | list of alias (at least one) | no | Options (or milestones) that end the work: a register opens on the rows not at them, and the badge reads muted |\n| `history` | alias | no | A child entity every status change appends one row to (its link to this entity, the new status, the moment and who) \u2014 written by the app\'s own writes, never a table automation; a row the model seeds opens it at its status, the day the rows land, unless the model states its rows; with `field` only |\n| `stages` | map of alias \u2192 [Stage](#stage) | no | Option alias \u2192 who owns that stage and how long it should take: the owner alone presses the acts offered there, refused on the server for anyone else. The status then moves only by acts that `set` one of its options \u2014 no save writes it and no act asks it; with `field` only |\n\n#### Milestone\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A date field: the stage is reached on the day it holds |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | yes | The stage applies only while each named select holds one of these options, and each named stored yes/no is this value; otherwise the row skips it |\n\n#### Stage\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `owner` | text \\| [Stage role](#stage-role) | yes | Who owns the stage: a member field of the row, `link.field` read through a one-row link, or `{ role }` \u2014 a group |\n| `within` | integer | no | Whole days the stage should take from the moment the row entered it |\n\n#### Stage role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `role` | alias | yes | A role of the model\'s `roles`: every member of the group it binds to owns the stage |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.status` | A status is one single select, `closed` naming options of it \u2014 or its `milestones`. |\n| `records.history` | A declared status `history` has exactly one link to the row it records, one single select holding every option of the status (or a text), exactly one date and at most one member. |\n| `records.history-derived` | A `history` the model does not declare is derived and keeps one entity\'s moves, labelled by that entity ("L\u1ECBch s\u1EED <entity label>", or "<entity label> history"): no declared table holds that label, and the entity has no other field under the link it adds. |\n| `records.history-scope` | A status history reads by its record\'s `read_scope`, answered through its one-row link to the record within the links a row rule reaches \u2014 or it states its own. |\n| `records.milestones` | Milestones are dates a person ticks, each named once; `closed` names some of them, and a deadline (`due`) is never one. |\n| `records.milestone-add` | An add asks the first milestone at most \u2014 each after it is ticked in order. |\n| `records.stages` | A stage\'s owner (`status.stages`) is a single member field of the row, one read through a one-row link, or a declared role, on an option that does not end the work; a role\'s stage needs the status `history` with who moved the row. A status with stages moves only by acts that `set` one of its options \u2014 no save writes it, and no act asks it, sets it otherwise or writes it from a workflow \u2014 and the moment its row entered its stage is kept in `<status>_since`, derived beside it, which nothing else writes. |\n| `records.history-note` | *Noted, never refused:* A record, never a task, whose stored status an act of its app moves, with no `history`: when a row entered each status and who moved it is kept nowhere. |\n| `records.detour-note` | *Noted, never refused:* A stage the work only steps aside into \u2014 not where a row starts, entered from one step alone that also moves the work on without it, and left by its own moves only back to that step (a pause, a repair) \u2014 is a detour: its option and the section staged at it stand before that step\'s, else the work reads it as its next step, and the act moving the work into it leads that section \u2014 never a correction waiting in a \u22EF. |\n\n<!-- generated:end status -->\n\n- **The status** is one single select. `closed` options end the work: a register opens\n on the rows not in them. A `history` entity gets one row per move \u2014 its link to this\n entity, the new status (a select holding the same option aliases, or text), the\n moment and who \u2014 appended by the app\'s own writes that move the status, never by a\n table automation. A `history` the model does not declare is derived, and needs no\n `records` entry of its own.\n- **Milestones** are the alternative to a stored select: dates in the order the work\n reaches them, the stage being the last one filled (never stored). A milestone with a\n `when` applies only while its select holds those options and its stored yes/no that\n value; otherwise the row skips it.\n The record draws them as one block where ticking stamps today and clearing one clears\n every later one; every save and act re-checks that a date comes after each earlier one\n that applies. `closed` names the milestones that end the work. A register\'s add may ask\n the first milestone, never a later one; an act may stamp one, never clear it.\n\n## Write rules\n\n`write_rules` is what a WRITE meets, keyed by entity alias; each entity\'s is held\nby its table. Every generated write of an\napp (`create_<entity>`, `update_<entity>`, each act) re-checks these on the\nserver; the screen only mirrors them.\n\n```jsonc\n"write_rules": {\n // A customer is RECOGNISED by their address. An add that names one \u2014 from the\n // customers register or from a picker\'s "new" \u2014 reuses the row it matches and\n // opens one only where nothing does, so the book never grows a second Acme.\n "customer": { "natural_key": ["email"] },\n "order_line": {\n "fields": {\n // A line of nothing is not a line. Refused on create and on update.\n "quantity": { "min": 1, "max": 9999 },\n // Shipped inside its order\'s window: a bound read off the row a link names\n // is held from both sides \u2014 moving the order\'s dates never strands a line.\n "ships_on": { "min": "order.placed_on", "max": "order.due_on" },\n // Nothing is sold off an empty shelf. The picker reads only the rows that\n // answer this, and the write refuses the same rows again.\n "product": {\n "options_where": {\n "node_type": "group", "logic": "and",\n "children": [{ "node_type": "condition", "type": "number",\n "field_key": "in_stock", "operator": "greater_than", "value": 0 }]\n }\n }\n }\n },\n // One booking per room at a time, while it is held: a create or a save whose\n // span overlaps another held booking of the same room is refused.\n "booking": {\n "fields": { "ends_on": { "min": "starts_on" } },\n "no_overlap": { "from": "starts_on", "to": "ends_on", "by": ["room"], "while": { "state": ["held"] } }\n }\n}\n```\n\n<!-- generated:start write-rules -->\n\n#### Entity write rules\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `natural_key` | list of alias (at least one) | no | The field aliases a row of this entity is RECOGNISED by. A write that names a row of this entity by these values reuses the row it finds, minting one only where nothing matches. |\n| `fields` | map of alias \u2192 [Field write rule](#field-write-rule) | no | Field alias \u2192 what that field\'s value is bounded by or picked among |\n| `no_overlap` | [No overlap](#no-overlap) | no | No two rows hold overlapping spans \u2014 a create or update that would is refused |\n\n#### Field write rule\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (its notation\'s `per_row`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n| `options_where` | [filter](#views) | no | Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET entity\'s own fields, each `field_key` a field alias. It narrows the picker\'s read and is refused again where the write lands. |\n| `same` | list of alias (at least one) | no | One-row links this row and each row this link points at must name alike \u2014 a fee\'s invoice is one of its own visit\'s, a box\'s seal one of its own shipping line\'s: each a one-row link of both entities to the same entity. It narrows the picker\'s read to the rows naming what this row names, and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare. |\n| `suggest` | text | no | A text field whose control offers, as the reader types, the distinct values a text field of a catalog holds \u2014 `<entity alias>.<field alias>` (a damage position\'s code among the codes on file); any text is still written |\n\n#### No overlap\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The stored date (or datetime) a row\'s span starts on |\n| `to` | alias | yes | The stored date (or datetime) a row\'s span ends on \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at |\n| `by` | list of alias (at least one) | no | Fields two rows share to compete for a span (the same employee, the same room); absent, every row |\n| `while` | map of alias \u2192 list of alias (at least one) | no | Only rows at these options of these selects hold their span (running, not closed); absent, every row |\n\n<!-- generated:end write-rules -->\n\n<!-- generated:start rules-write -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `write.natural-key` | A `natural_key` is text or a number a person types back, or a one-row link; a text key is unique \u2014 `unique: true`, or the key\'s fields as a set in the entity\'s `unique`. |\n| `write.bounds` | `min` and `max` bound a number or a date \u2014 by a constant a number, by a field of this row or of the one row a one-row link names either, of the same type and unit, never by itself \u2014 and `min` is never above `max`. |\n| `write.bound-per-row-unit` | On a figure whose unit or currency is the row\'s own (its notation\'s `per_row`) a constant bound is only 0 \u2014 bound it by a field of the row. |\n| `write.suggest` | `suggest` stands on a stored text field and names `<entity>.<field>`, a stored text field of a declared entity; the app reads that entity, so it has a `records` entry. |\n| `write.same` | `same` stands on a link and names one-row links this entity and the link\'s target both hold to one entity. |\n| `write.options-where` | `options_where` narrows a link by an `and` group of plain conditions over the target\'s own fields \u2014 a number compared in one unit on every row, a select by options it declares. |\n| `write.no-overlap` | `no_overlap` spans two date fields; `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options. |\n\n<!-- generated:end rules-write -->\n\n- A `natural_key` is `text` or `number`, because a person types it back, or a\n one-row link, because a row is also recognised by the one row it belongs with (a\n fee by its visit and its kind). A key holding a `text` field is unique: `unique:\n true` on that field, or the key\'s fields as a set in the entity\'s `unique` (a\n site by its customer and its name) \u2014 two rows sharing it would make\n find-or-create pick whichever the read answered first. An add asks the key by\n default.\n- A `min` or `max` naming a field is read off the row as the save would leave it,\n and one naming `<link>.<field>` off the row the link names \u2014 the link\'s side\n re-checks its own rows when that bound moves.\n- A `same` link names only rows that name what this row names \u2014 a fee\'s invoice\n is one of its own visit\'s \u2014 through a one-row link both entities hold to one\n entity; its picker offers nothing until the row names one. A row added under a\n record names what it copies off it (`default_from`) from the start, so a block\n expecting one line per row of the link lists that record\'s rows alone. A save\n moving what the two compare, on either row, is refused while they would name apart.\n- An `options_where` link may be left empty; a row it names is refused again\n where the write lands unless it holds the narrowing. A link naming one kind of\n a book\'s rows (a "Shipping line" among the parties) narrows to that kind, or\n its picker offers every row.\n\nA field\'s own `required` is not here: the contract carries it, and every write\npath refuses the row by field name from it. `unique` is the text field\'s own\nclause (\xA7 `text`) \u2014 an add says so at the control before the column does. A\nfield the workspace writes (a formula, a rollup, a lookup, an autonumber, a date\nwith `derive_from`) is never asked, written or edited.\n\n## Apps\n\nAn app is ONE register over one entity and the record each row opens \u2014 or a dashboard\nof readings. The author states COMPOSITION \u2014 which fields go where; the runtime owns\nhow each thing looks, one treatment per concept. Nothing is drawn that the app does not\nname, and each fact appears once on a record. What the vocabulary has no word for is an\nact\'s own `workflow`.\n\n```jsonc\n"apps": [{\n "alias": "gate", "name": "Gate in and out", "entity": "visit",\n "register": { "columns": ["service"], "filters": ["customer", "service"] },\n "record": {\n "sections": [\n { "title": "In", "fields": ["customer", "service"], "blocks": [{ "rows": "fee", "columns": ["amount"] }] },\n { "title": "Out", "at": ["in_yard"], "fields": ["release", "seal"], "blocks": [{ "files": ["photos"] }], "acts": ["gate_out"] }\n ]\n },\n "acts": [{ "alias": "gate_out", "label": "Gate out", "when": { "stage": ["in_yard"] },\n "requires": ["release", "seal"], "set": { "stage": "gone", "left_on": "now" }, "confirm": true }],\n "checks": [{ "field": "unpaid", "blocks": ["gate_out"] }]\n}]\n```\n\nThe keys below are the app\'s own. Each part it composes is a page of its own: `model/register`,\n`model/layouts`, `model/tabs`, `model/export`, `model/readings`, `model/dashboards`, `model/record-page`,\n`model/blocks`, `model/composed-page`, `model/publish-desk`, `model/timeline-and-agenda`, `model/statements`, `model/reading-blocks`, `model/tasks`, `model/acts`,\n`model/papers-and-recordings`, `model/checks`.\n\n<!-- generated:start apps -->\n\n#### App\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `entity` | alias | yes | The entity whose rows this job works \u2014 the register\'s rows |\n| `books` | list of [Book](#book) (at least one) | no | Other entities the register reads as one list with `entity`\'s rows, each opened, edited and acted on in its own table: the same app over each, its fields lined up with `entity`\'s. Only a table or cards |\n| `scope` | alias | no | A required one-row link of the entity: the app works inside one row of the linked entity at a time (a project, a branch), picked from a list of them, and every row it reads, counts and adds is that row\'s |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `writes` | `"children"` | no | The reader adds and edits the record\'s child rows but not the record\'s own fields; the app\'s acts still move it |\n| `row` | [Row display](#row-display) | no | How this job reads the entity\'s rows where it reads them otherwise than `records` (a cashier\'s figure is what is owed): the subtitle, figure and image stated replace the entity\'s wherever this app draws its rows \u2014 the register, a card, the record\'s header, a calendar, lane or roster entry. The title stays the entity\'s |\n| `register` | [Register](#register) | no | The rows: which columns and filters, the order, how a row is added |\n| `record` | [Record page](#record-page) | no | What a row opens: the door, its sections, and the Activity its head opens \u2014 its saves, and its comments where stated |\n| `acts` | list of [Act](#act) | no | What the reader does to a record \u2014 each a press with its conditions and its write |\n| `checks` | list of [Check](#check) | no | Formulas that warn while they stand, or refuse the acts and saves they name |\n| `declines` | map of text \u2192 text | no | Decided rules this app stays outside of, each with why in a line: by id, and `records.picture:<entity>` and `model.template-act:<template>` with what they fire on. Read by the apply\'s check alone, never by the app |\n\n#### Theme\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `color` | text | yes | The tile\'s colour |\n\n#### Row display\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title in this app, in place of the entity\'s |\n| `figure` | alias | no | The one number a row stands for in this app, in place of the entity\'s; a `limits` bound on it reads as its meter |\n| `image` | alias | no | A files field that pictures a row in this app, in place of the entity\'s |\n\n#### Book\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | Another entity whose rows the register reads beside its own \u2014 the same job over another table (a legacy ledger, a second branch\'s book) |\n| `fields` | map of alias \u2192 alias | no | A field of the app\'s entity \u2192 the field of this entity holding the same fact, where their aliases differ; a field the app reads lines up by its own alias otherwise, and one this entity lacks is blank on its rows and drops from what they open \u2014 an act, check or add needing it is not offered on them. Options line up by alias |\n\n<!-- generated:end apps -->\n\n<!-- generated:start rules-app -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.alias-unique` | An app\'s alias is unique among the model\'s apps, and an act\'s among its app\'s acts. |\n| `app.records-entry` | Every entity an app lists, opens or picks \u2014 its own, a block\'s, a linked row\'s, a document\'s \u2014 has a `records` entry. |\n| `app.reads-shared` | `reads: "shared"` stands only where a table the app reads states a `read_scope`. |\n| `app.writes-children` | An app that writes only the record\'s children (`writes: "children"`) adds no row of its own entity. |\n| `app.scope` | `scope` is a required one-row link of the app\'s entity, the same on every row the app lists \u2014 never a column, filter, search field, group, `where`, `opens`, section field or add question. |\n| `app.when` | A `when` holds single selects to options and yes/nos to `true` or `false`, stored or a formula of one (a formula with `options` is a select) \u2014 a milestone\'s `when` only a stored select or yes/no, read before any formula over it is computed. |\n| `app.asked-written` | What an add, an act or `starts` fills is a field a person writes \u2014 never a formula, rollup, lookup, autonumber or a date the workspace stamps. |\n| `app.own-rows` | A many-link to rows that each belong to one row of this entity is never a section\'s field, an add\'s question or an act\'s ask: those rows are the record\'s own, read as a `rows` block. |\n| `app.create-asks` | An add asks fields of a table people write \u2014 never the link or the folder it files the row under, which the add sets itself. |\n| `app.create-required` | An add (`create`) asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add. |\n| `app.books` | A register app\'s `books` each name another entity once \u2014 never the app\'s own \u2014 whose fields line up with the app\'s by alias, else by `fields` (a field the app reads, or one a report prints \u2192 one of the book\'s): read as one column type (a day apart from a moment), one value or several alike, a link to the same entity. Each book holds the title, the status, the folder (`scope`), what `where` keeps and every check locking the rows; the register is a table or cards. Two books holding a field of their own under one alias hold it as one kind of value. A report\'s `books` each name the app\'s entity or one of its books, once, and one of them holds each field its `where` and `per` narrow by. `register.book` reads a register that has books. |\n| `app.book-option-kin` | *Noted, never refused:* A book\'s option labelled as one of the app\'s, under another alias, reads apart from it: the register lists both and counts each on its own. |\n| `app.book-lacks` | *Noted, never refused:* A book lacking a field the app\'s record, an act or an add reads: what reads it drops from the book\'s rows \u2014 an act, a block, a section, the add. A field only a report prints, held by a book as another kind of value, prints blank on its rows. |\n| `app.row` | An app\'s `row` states only what differs from `records`: a line that is not the title, a number for its figure, a files picture no act makes documents into. |\n| `app.files-shown` | Every files field is drawn by some app over its entity \u2014 a section\'s `fields`, a `files` block, a column, the `image`, or where an act making its papers into it (`into`) stands, a section naming the act: the made paper reads under the act, one per template. |\n| `app.tasks` | *Noted, never refused:* A task entity an app lists whose rows no write of the app moves to a closed option (its ring stands disabled), or that an act ticks done and none moves back. |\n| `app.declines` | `declines` names decided rules that fire on the app, each with why the app stays as it is: by id, and `records.picture:<entity>` and `model.template-act:<template>`, said once per entity and per template, with what they fire on. |\n\n<!-- generated:end rules-app -->\n\n`writes: "children"` is a desk that adds and edits a record\'s child rows and never\nthe record itself. `reads: "shared"` lifts every read scope the app\'s tables state \u2014\nrefused where none of them states one.\n\n`scope` names a required one-row link of the entity: the app works inside ONE row of\nthe linked entity at a time \u2014 a project, a branch, a season. With no folder remembered\nit opens on the list of them, each with how many of the app\'s rows it holds; inside,\nevery read, count and reading narrows to that one, an add files its row under it (the\nlink is never asked), and the record states its folder in the header. A scope narrows\nthe view; who may read what stays the entity\'s `read_scope`. The folder link is never a\ncolumn, a filter, a search field, a fact or an add\'s question.\n\nAn app whose job reads its rows otherwise than the entity\'s `records` restates them in\nits own `row` \u2014 the cashier\'s figure is what is still owed, its line the service and the\nday. The subtitle, figure and image it states replace the entity\'s wherever that app\ndraws a row of it; every other app reads `records`, and the title is the entity\'s.\n\n### Declines\n\nA rule the check marks *Refused until adopted or declined* is a treatment the apply requires unless the app\ndeclines it. The apply refuses an app missing one and hands it a patch stating it: merge the patch, or state in the app\'s `declines`\nwhy the app stays as it is, under the key the finding names \u2014 the rule\'s id, or `records.picture:<entity>` and\n`model.template-act:<template>` with what they fire on. A decline naming a rule that is not decided, or one no\nrows of the model could fire, is refused. Only the apply\'s check reads `declines`; the app never does. Which\ntreatment each rule stands for: the `design` reference.\n\n```jsonc\n"declines": { "register.span": "The yard plans by the day each hire is due back, never by its run of days" }\n```\n\n## Register\n\n```jsonc\n"register": { "columns": ["service", "release"], "filters": ["customer", "service"], "create": ["container_no", "customer"] }\n```\n\n<!-- generated:start apps-register -->\n\n#### Register\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `columns` | list of alias | no | Fields read as columns after the row\'s title \u2014 on a calendar\'s entry or a lane\'s block, its facts after its title; never what the row draws itself (its subtitle, image, status, figure, or the bound its figure\'s meter reads against), which the runtime places |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The fields the reader narrows the rows by every day, in that order \u2014 each earns its place by that daily use, so none to 3 is normal and 3 is a cap, never a quota (a select, a member, a link, a date, a yes/no, a number as a range, or its `tiers` where the business narrows by those bands daily) \u2014 each on a desk\'s line; a phone\'s one Filters sheet holds those on a field its rows show there (their name, their line, the one reading beside the name). Never the status (its chips); each a field the rows show \u2014 a column, or a part of the row. A threshold the business names ("large orders") is a formula yes/no here; a column\'s header sorts by a day or an amount |\n| `book` | `true` | no | With `books`: a column naming the table each row is of, and a filter by it |\n| `search` | list of alias (at least one) | no | The fields the search box matches (a link by its row\'s title); a pasted list searches each line and names the lines no row matched. Absent, every word of the row |\n| `group` | alias | no | The field the rows open grouped by, the reader\'s grouping starting there \u2014 a period\'s rows read under each period, one per person; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order rows open in: one key, or a list of keys most significant first, each breaking the ties of the ones before it (a select orders by its options\' order); absent, the first deadline soonest first, else the newest first |\n| `layout` | `"table"` \\| `"cards"` \\| `"calendar"` \\| `"roster"` \\| `"lanes"` \\| `"gantt"` | no | `cards` for rows read by their picture; `calendar` for rows read by their day \u2014 a month, a week, a day or a list, an entry at its hour where its date holds one and over its span where its line holds a second date; `roster` for what each row did on each day of a week or a month (who worked which shift, which vehicle ran), or with `expect` which of a set each row holds; `lanes` for rows booked on a resource over time (an appointment in its chair, a hire on its machine); `gantt` for rows each planned over a run of days, one bar a row on one axis of time (a shipment from its sailing to its arrival, a hire from its start to its return, a task of a project) \u2014 its dates in `gantt`, its lanes the register\'s `group`; absent, a table |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the rows whose select holds one of these options and whose yes/no (stored, or a formula) is this value \u2014 read so on the server and never offered as a chip: the rows this app works of an entity other apps read whole (the purchases, of orders both ways). A row added here holds a select\'s one option and a stored yes/no\'s value |\n| `opens` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The view the register opens on: each entry a narrowing on a field the rows show that stands as a chip the reader may take away (the rows still owed), `"me"` a member field holding the reader (the rows mine). One on the status replaces the chips\' opening on the open work \u2014 `"me"` on a status whose `stages` have owners opens on the rows waiting on the reader |\n| `remove` | `false` | no | The app edits its records and offers no Delete on them (a row\'s \u22EF, its record\'s \u22EF) \u2014 a desk correcting a date of records another app makes and closes; absent, a record the app writes is deleted where it stands |\n| `create` | list of alias (at least one) \\| `false` | no | The fields asked when a row is added; absent, the title, the subtitle and figure no default fills, the natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `readings` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot) \\| [Progress](#progress)) (at least one) | no | Readings of the register\'s own entity above its rows, over the rows in view \u2014 the search, the status and the filters narrow them, but a picture\'s own field, read as the register opens on it. A press on a breakdown\'s part, a pivot\'s cell or a figure with `where` narrows the rows by its field, as a filter does |\n| `attention` | `false` | no | false: a table\'s or cards\' rows keep the register\'s own order (a first-come queue), and no column names whose step each row stands at; absent, those needing attention open first \u2014 those at a step the reader owns, then overdue, then due within 3 days, then those whose line states a warning \u2014 each part in that order, until the reader picks another |\n| `imports` | `true` | no | true: the register keeps a log of the runs of the workflows marked as importing into its table (a workflow\'s `imports_into`) \u2014 each run\'s end and the rows it added, changed and removed of those the reader sees; absent, none |\n\n#### Tiered filter\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row |\n| `tiers` | list of number (at least one) | yes | The breakpoints between its bands, ascending, in the field\'s stored unit: under the first, from each to the next, from the last up \u2014 each band worded in the field\'s notation, several picked at once |\n\n#### Sort key\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field rows are ordered by |\n| `desc` | `true` | no | Latest or largest first; absent, soonest or smallest first |\n\n<!-- generated:end apps-register -->\n\n<!-- generated:start rules-register -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.column-drawn` | A column never names what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 and names a field once. |\n| `register.filter` | A filter names a select, a member, a link, a date, a yes/no or a number of the register\'s own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes. |\n| `register.filter-split` | *Noted, never refused:* A filter on a field a breakdown or pivot in the band splits the rows by \u2014 a press on its part narrows by it too; a member\'s filter, which narrows to the reader\'s own rows, is never noted. |\n| `register.filter-unshown` | A filter, a narrowing the register `opens` on, and a press in its band (a breakdown\'s part, a pivot\'s cell, a figure\'s `where`) name a field its rows show \u2014 a column; the row\'s title, the line under it, its picture, status or figure, a meter\'s bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under. A narrowing to the reader (`"me"`) needs none. |\n| `register.tiers` | `tiers` band a number read in one unit on every row; a figure read in each row\'s own unit or currency is named bare. |\n| `register.search` | `search` names text, a code, or a link \u2014 matched by its row\'s title. |\n| `register.sort` | Each `sort` key names a field of the register\'s rows that holds an order \u2014 never files \u2014 and no field twice. |\n| `register.group` | `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and a gantt into its lanes; grouping by the title needs a subtitle to lead each row. |\n| `register.readings-own` | A register\'s `readings` read its own entity; another entity\'s stand on a dashboard. |\n| `register.readings-restate` | A register\'s metric never sums the row\'s figure or counts the rows without a `where`: the summary line totals the figure and the status chips count the rows. |\n| `register.where-opens` | `where` and `opens` hold the register\'s own selects and yes/nos, and `opens` a member field holding the reader (`"me"`) \u2014 or, on a status whose `stages` have owners, the rows waiting on the reader; a field `where` fixes is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. |\n| `register.band-pivot` | *Noted, never refused:* A pivot in a register\'s band: its grid reads on a dashboard, and the band leads with a `metric`. |\n| `register.headline` | *Refused until adopted or declined:* A register over rows that move (a status, a deadline) or carry money, laid out as a table, cards or a gantt, states `readings` \u2014 the patch splits the rows by a field they show and reads them over a date. |\n| `register.span` | *Refused until adopted or declined:* Rows that each span a run of days or hours \u2014 two dates on their line, a `frees`, or a `no_overlap` booking them on what its `by` links to \u2014 are drawn in time by some register over them (a calendar, lanes, a gantt or a roster) \u2014 the patch adds an app over them laid out as lanes on what each one books, else as a gantt. |\n| `register.remove` | A register\'s `remove: false` hides the Delete its records would offer, so it stands only where the app writes its records. |\n| `register.attention` | `attention: false` keeps a table\'s or cards\' rows in the register\'s own order \u2014 a first-come queue; a calendar, lanes, a gantt and a roster stand their rows in time and lift none. |\n\n<!-- generated:end rules-register -->\n\nThe status is always the chips, with\ncounts, opening on the rows not closed (every row on a calendar or a lanes board, whose\nwindow of time narrows them) \u2014 never one of `filters`. `filters` are the ones a reader\nnarrows by every day \u2014 none to three, never a quota \u2014 in order of use, each a field the\nrows show (a column, a part of the row), each on the line\nwith the search and the chips, the grouping one\nchip at its end; a member filter offers "mine". A date filters by a range of days \u2014 of\nminutes on a `datetime` field, its end not held, so back-to-back shifts read each row\nonce \u2014 and can keep the rows holding no date too (in the yard at a moment: in before it,\nout after it or not yet). A number filters by a range typed in its\nunit or currency \u2014 a figure read in each row\'s own, in the one picked beside it \u2014 with a\nslider over a percent or a figure against a constant `limits`; `{ "field", "tiers" }`\noffers the bands its breakpoints cut in place of a range (`[1000, 5000]` on a weight in\nkg: under 1.000 kg, 1.000\u20135.000 kg, 5.000 kg and over), refused on a figure read in each\nrow\'s own unit. A threshold with a name is a formula yes/no, never a range. A\ncolumn the row already draws is refused. `sort` is one key or a list of keys, most\nsignificant first, each breaking the ties of the ones before it \u2014 a shipping line in its\nselect\'s option order, then the arrival day soonest first:\n`[{ "field": "shipping_line" }, { "field": "arrived_on" }]`; a header\'s press leads them.\nAbsent `sort`, rows open soonest deadline first where the row has a `due`, else newest first. `search` names the fields\nthe search box matches \u2014 a link by its row\'s title \u2014 and a pasted list searches each\nline, naming the lines no row matched; absent, it matches every word of the row.\n\n`where` keeps the rows the app works, read so on the server and never a chip: a\npurchase desk over orders of both directions states `"where": { "direction":\n["purchase"] }`, and a row added there is written holding the one option (a select kept\nat several is asked among them). `opens` is the view the register opens on, each entry a\nchip the reader may take away: a cashier opens on `"opens": { "owes": true }`. Both take\na select\'s options or a yes/no\'s value, stored or a formula; `opens` on the status is\nthe chips\' opening choice. `opens` also takes `"me"` on a member field \u2014 `"opens": {\n"assignee": "me" }` opens on the rows assigned to whoever reads, the server reading the\nreader, never the page; `where` refuses it, since it fixes the rows for every reader\n(who may read a row at all is the entity\'s `read_scope`).\n\nHow the rows are laid out is \xA7 Layouts, tabs over a period \xA7 Tabs, the report \xA7 Export, and the band above\nthe rows \xA7 Readings.\n\n## Layouts\n\n`layout` is how the register draws its rows \u2014 one of the pages below; absent, a table. Each reads what the\nrows already state (their `records` line, status and dates), and the keys a layout adds stand on its page.\nWhich one a job needs: the `design` reference.\n\n### Table\n\nAbsent `layout`, each row is one line: the entity\'s `records` entry \u2014 its picture or mark, title and the line under it\n\u2014 then the status, the `columns`, and the figure at the right; the title, the status and a column of days or\namounts sort the rows by their header. A table groups its rows by `group` (a gantt draws it as its lanes); a\ntable or cards reads `tabs` and `books`.\n\n### Cards\n\n`layout: "cards"` draws each row as a card led by its picture \u2014 the `records` `image`, or the app\'s\n`row.image`: its title and the line under it, its status and what needs the reader (a deadline, a warning),\nthen its figure and the register\'s `columns` at its foot. Cards are for rows found by their picture (a\nproduct, a vehicle, a damage photo); rows with no picture read better as a table.\n\n### Calendar\n\n`layout: "calendar"` places each row on its day \u2014 a month, a week, a day or a list (a\nphone reads the list or a day): at its hour where the date holds one, over its span\nwhere the row\'s line holds a second date after it, its line\'s other words and the\nregister\'s `columns` after its title. The calendar reads the window in view, and its\ncount and the status chips count that window, opening on every row.\n\n### Lanes\n\n`layout: "lanes"` with `lanes` naming a one-link draws each row of the linked entity as\na lane (a chair, a machine, a room), empty ones too, and each register row as a block\nfrom the first date on its line to the second: over the hours of one day where the first\nholds its hour, else over the days of one day, a week or a month. A block reads the row\'s\nname and the first of its `columns`; a free stretch between blocks adds a row there, its\nlane and its start set, and its end where the next block bounds it. A phone lists each\nlane\'s blocks and free stretches in clock order. Rows naming no lane stand first, in a lane\nof their own; where the app writes the link, each block moves to another lane from its menu.\nInside a folder (`scope`), the lanes are the ones whose own one-link names that folder \u2014 a\nbranch\'s rooms, never every branch\'s. A span\'s end holds its day unless `records` names it\nthe day the row `frees`.\n\n`loads` names what a lane carries: `{ "weight": "payload", "volume": "box" }` sums each\nblock\'s number and reads it against the lane row\'s own, at most two. A lane holds its\nblocks while they stand, so it reads the most they add up to at one time in the window \u2014\na day\'s rows together \u2014 named, past its bound, by how much and on which day or at which\nhour. The two are in one unit, or two of one dimension (kg against t); a block then reads\nwhat it adds, and the move menu reads each lane as it would stand with the block on it.\nA bound a single row must keep (no parcel heavier than its van) is a `write_rules` `max`\nthrough the link, which refuses the move.\n\n<!-- generated:start apps-lanes -->\n\n#### Lanes keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `lanes` | alias | no | With `layout: "lanes"`: a one-link of this entity whose target\'s rows are the lanes (a chair, a machine, a room), each drawn even when empty. A row is a block from the first date on its line to the second \u2014 within one day by the hour where the first holds one, else by the day over one day, a week or a month; a free stretch between blocks adds a row there, its lane, its start and the end the stretch reaches filled |\n| `loads` | map of alias \u2192 alias | no | With `lanes`: what a lane carries against what it holds, at most 2 \u2014 a number of this entity each block adds (a weight) to a number of the lane\'s row it is held within (a payload), in one unit or two of one dimension. Each lane reads the most its blocks add up to at one time in the window \u2014 a day\'s rows together \u2014 and names by how much and when a lane runs over |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.lanes` | `layout: "lanes"` and `lanes` go together: `lanes` is a one-row link to an entity with a `records` entry, the rows name a date on their line, a block\'s two ends are read alike (two days or two moments), and `loads` needs `lanes`. |\n| `register.loads` | `loads` names one to 2 numbers a block adds \u2014 never a percent \u2014 each against a number of the lane\'s row, in one unit or two of one dimension. |\n\n<!-- generated:end apps-lanes -->\n\n### Gantt\n\n`layout: "gantt"` with `gantt` draws each register row as one bar on one axis of time:\n`{ "start": "etd", "end": "eta", "milestones": ["cut_off"], "planned": { "end": "booked_eta" },\n"progress": "done", "after": "waits_on" }`. `end` defaults to the entity\'s first `due`, and an\nentity with neither is refused; `start` and `end` are two dates of the row, both days or both\nmoments, and each planned date is read as `start` is. `progress` is a percent (0\u2013100) \u2014 absent, a\nrow whose record lists its tasks fills to how many are done;\n`after` is a link to rows of the same entity, one or several, each ending before the row\nstarts. The bars stand in the lanes of the register\'s `group`. Where the app writes the\nrow and a person writes a bar\'s date, the bar is dragged through the row\'s save, which then\nwrites those two dates though no section places them; a computed date, one an act sets, or\nan app with `writes: "children"` drags nothing.\n\n<!-- generated:start apps-gantt -->\n\n#### Gantt keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `gantt` | [Gantt bar](#gantt-bar) | no | With `layout: "gantt"`: the dates and facts each row\'s bar is drawn from |\n\n#### Gantt bar\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of this entity each row\'s bar starts at (a sailing, a hire\'s start, a task\'s start) |\n| `end` | alias | no | The date each row\'s bar ends at, read as `start` is \u2014 two days or two moments; absent, the entity\'s first `due` |\n| `milestones` | list of alias (at least one) | no | Dates of the row marked on its bar\'s line as diamonds, each named by its label (a cut-off, a delivery, the end of free time) \u2014 never `start` or `end` |\n| `planned` | [Gantt plan](#gantt-plan) | no | The plan each row is read against, drawn as a thin bar under its own \u2014 each date read as `start` is; a missing one is the bar\'s own |\n| `progress` | alias | no | A percent of this entity (0\u2013100) filling each row\'s bar as far as the work is done; absent, how many of the row\'s tasks are done fills it where its record lists them, else the bar wears its status\'s tone |\n| `after` | alias | no | A link of this entity to its own rows each row waits on \u2014 it starts once they end (finish-to-start), an arrow from each; a row starting before one ends is drawn in the danger tone |\n\n#### Gantt plan\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The date the row was planned to start at |\n| `end` | alias | no | The date the row was planned to end at (a booked arrival) |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.gantt` | `layout: "gantt"` and `gantt` go together: `start` and `end` (absent, the entity\'s first `due`) are two different dates of the row, both days or both moments; each milestone another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity. |\n\n<!-- generated:end apps-gantt -->\n\n### Roster\n\n`layout: "roster"` draws the register\'s rows down the side and the days of a week or a\nmonth across; `roster` names the entity whose rows fill the days \u2014 each stands on one\nregister row by its single link to it, and on the first date of its line (a shift on a\nperson\'s day, a run on a vehicle\'s), on every day through the second date where its line\nholds one. A cell reads its row\'s status, else the first single select on its line (a\nshift\'s kind), several rows their count; a week\'s cells say their words and one more\nfact of the line, a month\'s keep their marks. A day holding none stays empty: nothing\nwas due, which is never an absence. A filled cell opens its row; an empty one adds a row\nthere, the register row and the day set, asking what any add of it asks \u2014 unless no\napp writes those rows (`writes: false`). With `expect` (a single select of those\nrows) the columns are its options in place of days \u2014 a checklist of papers \u2014 and `of`\n(this entity\'s multi-select) names the ones each row expects: a gap among them reads as\none and adds its row, the others stand blank, and each row reads what it holds of them,\n`2/3`. Over days, a roster reads the `cadence` its entity\'s `records` states for its\nrows (a site\'s diary page, Monday to Saturday from breaking ground to handover): a past\nday owed with none is marked missing in the danger tone and counted, today\'s is still\ndue and offered to add, each row reads the days it holds of those owed, and the days\nthe cadence lists none on are quiet. A roster states no `columns` and needs no\n`readings`: its legend counts each state in view.\n\n```jsonc\n"register": { "layout": "roster", "roster": "run", "filters": ["operator"] }\n"register": { "layout": "roster", "roster": "diary_page" }\n```\n\n<!-- generated:start apps-roster -->\n\n#### Roster keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `roster` | alias | no | With `layout: "roster"`: the entity whose rows fill the days \u2014 one single link to this entity, and a date on its row line (`records` title or subtitle). A cell reads its row\'s status, else the first single select on its line, several rows a mark each, the cell read by the one latest in that select\'s options; a row whose line holds a second date after the first fills every day to it; a day with none stays empty, never an absence \u2014 unless this entity\'s `records` `cadence` owes one of them there |\n| `expect` | alias | no | With `roster`: a single select of the roster\'s rows whose options are the columns in place of days \u2014 each cell the row holding that option, read by its status, an empty one added there (a checklist of papers per case); the roster then has no period |\n| `of` | alias | no | With `expect`: this entity\'s own multi-select holding the options each row expects \u2014 its other columns stand blank on that row, never added. Its options are among `expect`\'s |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.roster` | `layout: "roster"` and `roster` go together: the roster\'s entity has one single link to this one and a date on its line, a roster draws no `columns`, `expect` needs `roster` and `of` needs `expect`. |\n| `register.roster-expect` | A roster\'s `expect` is a single select of its rows, and `of` this entity\'s multi-select whose every option is one of `expect`\'s. |\n\n<!-- generated:end apps-roster -->\n\n## Tabs\n\nA register reporting a period \u2014 what stands at its end beside what came and went during it (the stock, the\nhires out, the tickets open) \u2014 reads its rows as `tabs` over one `period` the reader picks above them: an\n`open` tab holds the rows open at the period\'s end, an `in` tab the rows dated within it, each with its count\nand each a sheet of the report. Never date chips set by hand, and never a tab per status, which the chips split.\n\n```jsonc\n"register": { "period": "month", "tabs": [\n { "tab": "in_yard", "label": "In the yard", "open": { "from": "arrived_on", "to": "left_on" } },\n { "tab": "arrived", "label": "Arrived", "in": "arrived_on" },\n { "tab": "left", "label": "Left", "in": "left_on" }\n] }\n```\n\n<!-- generated:start apps-tabs -->\n\n#### Tabs keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tabs` | list of [Register tab](#register-tab) (2\u20135) | no | The lists the register reads its rows as, each a tab with its count over one period the reader picks above them \u2014 the stock at its end beside the arrivals and departures during it, a report\'s sheets; each keeps the register\'s filters and search. Absent, one list. Not with a calendar, roster or lanes, which read their own window |\n| `period` | `"today"` \\| `"week"` \\| `"month"` \\| `"quarter"` \\| `"year"` \\| [Period days](#period-days) | no | With `tabs`: the period they open on \u2014 `today`, `week`, `month`, `quarter` or `year` so far, or `{ days }`, the last that many days through now (a night shift running past midnight reads two); absent, this month so far |\n\n#### Register tab\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tab` | alias | yes | The tab\'s name: its address, and the key its rows stand under in a report |\n| `label` | text | yes | What the tab holds, in the reader\'s words |\n| `in` | alias | no | A date (stored, or a formula): the tab holds the rows dated within the register\'s period, from its start through its last day, or up to its last minute \u2014 the arrivals of a period |\n| `open` | [Tab open](#tab-open) | no | The tab holds the rows open at the period\'s end: begun before it, and not ended by then \u2014 what is in stock, out on hire or still owed at that moment |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order this tab\'s rows read in, as the register\'s `sort`; absent, the register\'s |\n\n#### Tab open\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The date a row begins on (stored, or a formula) |\n| `to` | alias | yes | The date a row ends on (stored, or a formula), empty while it is open |\n\n#### Period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | integer | yes | How many days through today, at most 366 |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.tabs` | A register\'s `tabs` each name a tab of their own, never a key a report already holds; a tab\'s `in` and `open` dates are dates of its rows, `open`\'s two different; its `where` and `sort` hold as the register\'s do, and never narrow by the status, which its chips split. Only a table or cards reads tabs, and a band reading\'s `tab` names one of them. |\n\n<!-- generated:end apps-tabs -->\n\n## Export\n\n`export` saves the register as its report \u2014 the title, a line per chip, the search and\nthe moment it was exported, the readings, and every row in view, at most 20,000 of them.\n`export: true` saves it as a workbook, the readings on a sheet before the rows. A list\nstates the reports the business sends \u2014 one a button, several one menu \u2014 each a `label`,\na `description`, and a `template` filled with `title`, `lines`, `readings`, `at` (the\nmoment it was made), `dates` (each date filter\'s `from` and `to` by field, a day or a\nmoment as the filter bounds the rows) and the rows under the entity\'s alias (an html one\nmade a PDF, an `excel` one a workbook; absent, the workbook). A report\'s `filename` names\nits file in the template grammar over the same keys but the rows \u2014\n`"Stock {{dates.arrived.to | format:\\"dd.MM.yyyy\\"}}"`; absent, the template\'s name, or\nthe label. A report of some rows (`where`), or of one value the reader picks (`per`: one\nline\'s file), first narrows the register as its chips, so the screen shows what the file holds.\n\n<!-- generated:start apps-export -->\n\n#### Export keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `export` | `true` \\| list of [Register report](#register-report) (at least one) | no | The rows in view saved as the register\'s report \u2014 its title, a line per thing narrowing the rows, the readings and every row: `true`, one workbook of the register\'s columns; a list, the reports a reader picks from one export menu |\n\n#### Register report\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | yes | The report\'s name, in the reader\'s words \u2014 its entry in the export menu and its file\'s title |\n| `description` | text | no | What the report holds, under its name in the menu |\n| `template` | alias | no | A template filled with the report \u2014 `title`, `lines`, `readings`, `at` (when it was made), `dates` (each date filter\'s `from` and `to`, by field), `period` (the tabs\' `from` and `to`), `per` (the value picked), every row in view (at most 20,000) under the entity\'s alias, and each tab\'s under its name: an html one made a PDF, an excel one a workbook; absent, the workbook of the register\'s columns, a sheet per tab |\n| `filename` | text | no | The file\'s name, in the template grammar over one value each \u2014 `title`, `at`, `dates.<field>.from` or `.to` of a date filter, `period.from` or `.to` with tabs, `per` with a `per`, `lines \\| lookup:<n>` \u2014 `Stock {{per}} {{period.to \\| format:"dd.MM.yyyy"}}`; absent, the template\'s name, or the report\'s title |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The rows the report is of, set on the register as the chips they are before its file is made \u2014 the screen shows what the file holds |\n| `per` | alias | no | A select, a member or a one-link the reader picks one value of from the menu \u2014 the rows narrowed to it, as its chip, and one file of that value |\n| `books` | list of alias (at least one) | no | The rows of these books only \u2014 each the app\'s `entity` or one of its `books`; absent, every book\'s |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.export-report` | A register\'s reports each have their own label; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes. |\n| `register.export-template` | A template an export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the register entity\'s alias \u2014 an entity named none of those \u2014 and is the document of no act. |\n\n<!-- generated:end apps-export -->\n\n## Readings\n\nA reading is one number, or one picture of numbers, over rows. Where it stands decides\nwhich rows: a dashboard reads every row, a register\'s `readings` the rows in view, a\nrecord\'s reading block the child rows under the record. The picture is the runtime\'s,\nchosen from what the reading means \u2014 no key picks a chart.\n\n```jsonc\n{ "metric": "visit", "value": "fees", "where": { "paid": false }, "over": "arrived_on", "label": "Unpaid" }\n{ "breakdown": "visit", "by": "line" }\n{ "trend": "visit", "over": "arrived_on", "value": "fees" }\n{ "pivot": "visit", "rows": "size", "columns": "line" }\n{ "list": "visit", "where": { "stage": ["in_yard"] }, "columns": ["line"], "app": "gate", "label": "In the yard" }\n{ "list": "task", "where": { "assignee": "me" }, "label": "My tasks" }\n{ "progress": "work_item", "planned": { "start": "plan_start", "end": "plan_end" }, "done": "complete",\n "finished": "accepted_on", "measured": { "rows": "logged_qty", "on": "day", "share": "share_of_item" },\n "weight": "value", "label": "Progress against plan" }\n```\n\nA reading\'s `where` keeps rows by a select\'s options, a yes/no\'s value, or `"me"` on a\nmember field \u2014 the rows that name whoever reads, read so on the server.\n\n`progress` reads one parent\'s work against its plan \u2014 a register\'s band over a folder, or a record\'s\nblock \u2014 never a dashboard\'s every row: the share of the work planned and the share done, cumulative day by\nday, today\'s two at its foot. Each row is a part of the work, its plan spread evenly over its planned days\nand weighing its `weight` (its value) or, absent, its planned days. Its share done today is `done`, a\npercent the server computes; on an earlier day, its `done` from its `finished` day on, before it what its\n`measured` rows add up to by then \u2014 each row\'s own share of it, a percent the server computes.\n\n<!-- generated:start apps-readings -->\n\n#### Metric\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n#### Breakdown\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n\n#### Trend\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n\n#### Pivot\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side |\n| `columns` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n\n#### Progress\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `progress` | alias | yes | The entity whose rows are the parts of the work, read as the share of it planned and the share done, day by day |\n| `planned` | [Progress planned](#progress-planned) | yes | The days each row\'s work is planned over, its share planned spread evenly across them \u2014 a row missing either is no part of the plan, and neither line counts it |\n| `done` | alias | yes | A number of the row the server computes: its share done today, in percent (0\u2013100) |\n| `finished` | alias | no | A date of the row: the day its work was finished, from which it stands at its `done` |\n| `measured` | [Progress measured](#progress-measured) | no | Before a row is finished, its share done on a day is what these add up to by then, never past its `done`; absent, a row stands at nothing until it is finished, and at its `done` today |\n| `weight` | alias | no | A number of the row: what it weighs in the whole \u2014 its value, its share of the contract; a row holding none weighs nothing. Absent, each row weighs its planned days |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n#### Progress planned\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of the row: the first day of its planned work |\n| `end` | alias | yes | A date of the row: the last day of its planned work |\n\n#### Progress measured\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows record the work as it is done, each through its one-row link to a row of the reading |\n| `on` | alias | yes | A date of those rows (their own, or a lookup): the day each records its work |\n| `share` | alias | yes | A number of those rows the server computes: the share of its row each adds, in percent |\n\n#### List\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `list` | alias | yes | The entity whose rows are listed, each in its `records` anatomy |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `columns` | list of alias | no | Fields read after each row\'s title |\n| `app` | alias | no | An app of this model over the same entity that a row opens in |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n<!-- generated:end apps-readings -->\n\n<!-- generated:start rules-reading -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.value` | A reading sums a number, never a percent \u2014 percents do not add up. |\n| `reading.split` | `by`, `rows` and `columns` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 and a pivot crosses two different fields. |\n| `reading.over` | `over` is a date: the row\'s own, stored or a formula, or its parent\'s through a lookup over a one-row link. |\n| `reading.where` | A reading\'s `where` keeps rows by a select\'s options, a yes/no\'s `true` or `false`, or a member field holding the reader (`"me"`). |\n| `reading.target` | `target` reads a value nothing bounds \u2014 a value `limits` bounds reads against the sum of its bound. |\n| `reading.better` | `better` needs `over`: a change is read over the periods its date places the rows in. |\n| `reading.list-app` | A list\'s `app` is a register app of this model over the list\'s own entity. |\n| `reading.progress` | The work against its plan reads dates of its rows for `planned` and `finished`, a number for `done` and `weight`, and rows `measured` through a one-row link to them, by a date of theirs and a number of theirs. |\n\n<!-- generated:end rules-reading -->\n\nA reading counts its rows, or sums the number `value` names \u2014 never a percent. `by`,\n`rows` and `columns` split the rows by one value each: a single select, a one-row link,\none member, or a yes/no \u2014 stored or a formula, its parts read as its label and the\nothers. `over` is a date, the row\'s own (stored or a formula) or its parent\'s through a\nlookup over a one-link: a trend\'s periods, a metric\'s recent periods beside its number,\nand what a dashboard\'s period windows. `ahead: true` reads a trend forward from the\ncurrent period over the rows still to come. `where` keeps rows whose select holds one\nof the options, or whose yes/no is the value, every entry ANDed \u2014 a band leads with the\njob\'s exception as a formula\'s yes/no (what is late, what is short), never a stage the\nstatus chips already count. A metric summing a value `limits` bounds reads against the\nsum of its bound (`10 / 14`); `target` reads one against a number nothing bounds. A\nmetric\'s change is green where it moves the good way: a sum\'s rise, a count kept by\n`where` falling; `better` states the other way where that reads wrong \u2014 money going out\n(`"better": "down"`). A register reads only its own entity; a `list` stands only on a\ndashboard.\n\n`readings` read the register\'s own rows above them: the rows in view, so the search,\nthe status and the filters narrow each one, but a picture\'s own field, which it reads as\nthe register opens on it. A table, cards or a gantt over rows that move or carry money states\nthem (`register.headline`); elsewhere they stand where a picture answers something the rows\ncannot. They stand under the title in one compact\nband: lead with the job\'s headline number (a `metric`), then at most the mix (a\n`breakdown`) and the movement (a `trend`) \u2014 a `pivot` stands under the band as a whole table, every line and its totals. A picture\nstands only where it splits the rows in view into two parts or more, one of them\nholding two rows or more; a split whose rows\nall fall in one part, and a lone figure with no picture beside it, are said on the\nsummary line instead, so a band stands only holding a picture or two figures. The summary line already totals the\nfigure over the rows in view and the status chips count them, so a `metric` summing the\nfigure, or counting the rows of an entity with a status, is refused unless its `where`\nnarrows it.\n\n## Dashboards\n\nA dashboard is `{ "alias", "name", "dashboard": [readings] }` with no entity, register\nor record: each reading reads every row of its entity, and one period the reader picks\nat the title \u2014 this month, last month, this or last quarter or year, or any days \u2014 windows\neach reading placed in time: one still running so far, one that closed whole, its change\nread against the period before it. A reading placed in no time reads the rows as they\nstand now, and its head says so. `period` is the one it opens on; absent, the month. A\ntrend holding few periods so far is led in by the ones just before it, so no period opens\non one bar.\nA `list` reading lists rows, each opening in its `app`. The readings answer the one\nquestion the dashboard\'s owner asks, in the order they ask it \u2014 rows to act on lead where\nthe answer is rows; no order of kinds is the rule.\n\n```jsonc\n// The depot owner\'s morning question: which boxes are past their free days, whose are they, is it growing?\n{ "alias": "overview", "name": "Depot overview", "dashboard": [\n { "list": "visit", "where": { "overdue": true }, "columns": ["line"], "app": "gate", "label": "Past free days" },\n { "breakdown": "visit", "by": "line", "where": { "overdue": true }, "label": "Past free days by line" },\n { "trend": "visit", "over": "arrived_on", "where": { "overdue": true }, "label": "Past free days, by arrival" }\n] }\n```\n\n<!-- generated:start apps-dashboard -->\n\n#### Dashboard app\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `dashboard` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot) \\| [List](#list)) (at least one) | yes | The readings, in order \u2014 each over every row of its entity, windowed by the one period the reader switches |\n| `period` | `"month"` \\| `"quarter"` \\| `"year"` \\| [Dashboard period days](#dashboard-period-days) | no | The period the switch opens on \u2014 `month`, `quarter` or `year` so far, or `{ days: 30 }`, the last 30 days through today; absent, this month so far. Needs a reading placed in time by `over` |\n\n#### Dashboard period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | `30` | yes | The last 30 days through today \u2014 the one span the switch holds |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.period` | A dashboard\'s `period` is the one its switch opens on, and the switch stands only over a reading placed in time by `over`. |\n\n<!-- generated:end apps-dashboard -->\n\n## Record page\n\n```jsonc\n"record": {\n "door": "page",\n "sections": [\n { "title": "Details", "fields": ["size", "line", "built_on"] },\n { "title": "Gate in", "at": ["arriving", "in_yard"], "fields": ["arrived_on", "truck"],\n "blocks": [{ "rows": "fee", "where": { "leg": ["in"] }, "expect": "kind", "columns": ["amount", "paid_by"] }],\n "acts": ["gate_in_paper"] },\n { "title": "Sell to a buyer", "description": "Moves the container to the buyer\'s stock once it has left.", "acts": ["sell"] }\n ]\n}\n```\n\n<!-- generated:start apps-record -->\n\n#### Record page\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `door` | `"page"` \\| `"drawer"` \\| `"beside"` | no | A page for work read at length; a drawer for rows worked one after another from the register; `beside` for rows worked one after another while the list stays in view \u2014 the register\'s list and the open record side by side, over a table register alone. Each stacks its sections, and from three sections on navigates them \u2014 a page by a rail, a drawer by tabs; absent, a page where a section has `at` or is a `page`, the record has more than three sections or one stands beside the rest (`side`), else a drawer |\n| `sections` | list of [Section](#section) (at least one) | no | The record in the order its work reaches it \u2014 each section its fields, then its blocks, then its acts. Absent, one section of every field a person writes that no other place shows |\n| `comments` | `true` | no | People comment on the record: its comments \u2014 who wrote what and when, with a composer \u2014 stand in its Activity, opened from its head; stated only where the job\'s people discuss a record or the owner asks for one. Absent, no one comments |\n| `history` | `true` | no | Draws nothing of its own: the record\'s Activity shows every move of the status as a change, when and who \u2014 refused beside `activity: false`, which shows the moves nowhere; noted beside Activity, and refused there in a later release. The entity\'s `status.history` is written either way |\n| `activity` | `false` | no | false: the app\'s records show no activity \u2014 the door in each record\'s head and the panel of its saves and comments beside it; where `comments` is stated, the door opens the comments alone. Absent, every record shows it |\n| `compose` | [piece](#composed-page) | no | The record\'s page stated as a tree of finished pieces instead of `sections`, on a page door \u2014 for a page that is its own arrangement of what the app already states: each node names a piece of the catalog (`use`) and binds by name to a field of the entity, an act of the app or a child entity; it holds no condition and no computed value |\n\n#### Section\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | text | yes | What the section is, in the reader\'s words \u2014 its heading, and its name on the record\'s rail or tab |\n| `description` | text | no | One sentence under the title: what the section is for, or what its act does |\n| `at` | list of alias (at least one) | no | Options of the record\'s status (or its milestones) during which this section is the work now \u2014 a step of the path the record moves through: its rail item reads now while the status holds one and this app has work there, done once the record\'s history says it went through one \u2014 or it is a step of the path before where that history opens (with none, where the record stands) that no act moves the work past \u2014 and it stands before the section that is the work now, and waiting otherwise; a section every option of which ends the work, past the path\'s end or entered by its own act, is an exit: no item and nothing drawn until the record stands in it, then set apart after the steps and never one; A page opened from the register opens at it. It never locks what the section holds: its fields are written as the write rules and checks allow at any stage. An act it names is offered during these alone: its `when` holds the status to them, or on milestones it stamps the step after one of them \u2014 a detour and an exit are sections of their own, a detour stated before the step it returns to |\n| `fields` | list of alias (at least one) | no | The record\'s own fields in this section, in the order they are read \u2014 never one the header draws: the row\'s title, subtitle, image, status or figure |\n| `blocks` | list of [block](#blocks) (at least one) | no | What the section holds besides its own fields, under them, in order |\n| `acts` | list of alias (at least one) | no | Acts of this app drawn under the section\'s fields, their asks as more of them and what each lacks said above its button; the header draws no act, so every act of the record itself is named by one section \u2014 an act of a child (`of`) stands on the child\'s row and is never named here. An act moving the status, offered at ONE step (the stage its `when` holds), stands at that step\'s foot when it moves the work forward \u2014 to a later option, an exit too \u2014 or back to a step the work passes anyway; one offered at several steps, or moving back into a detour, stands in the section it enters, which draws it while the work waits \u2014 an exit\'s in the header\'s \u22EF, since an exit stands only once the record is in it. A correction \u2014 `danger` and not one of its step\'s outcomes (no other status move offered at the same moment) \u2014 waits in the section heading\'s \u22EF, and one no section names in the header\'s \u22EF; every other act is named by a section |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `side` | `true` | no | The section the rest are worked from, drawn in a pane beside them rather than in their scroll \u2014 the photos looked at, the pool picked from, the paper typed off; at most one on a record, which opens on a page door. It holds what any section holds |\n| `page` | `true` | no | A page of its own, off the record\'s scroll: its own heading and a way back, reached from the record\'s rail after the scroll\'s sections, or from a link at its place where the record has no rail \u2014 for what makes the record worse stacked in it: a long list of related rows, a workspace of its own. Only on a page door, never a step (`at`). Holding only one `text` block over a markdown field, it reads as a document: a 680px column, saved as it is written \u2014 whether it is saved said at its foot |\n\n<!-- generated:end apps-record -->\n\n<!-- generated:start rules-record -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.placed-once` | A field, a block and an act each stand once on a record: the header draws the title, subtitle, image, status (or milestones), figure and folder, and a field\'s one other place is a section\'s `fields`, a files, text or statement block \u2014 save the image, which one files block may draw again, whole, the figure, which a statement draws as what it comes to, and rows one list prices and another compares (`compare`); two places whose `when`s never hold together (a select at options the other\'s lacks) each draw it. |\n| `record.foot-total` | A rows block\'s foot totals the child\'s figure and each summed column over the rows it lists; the record\'s rollup summing that field over those rows (the same `where`) is that foot, never also one of a section\'s `fields`. |\n| `record.fact-restates` | A section\'s field never looks up what its link already reads where it stands: the linked row\'s title and line beside the link, the figure or bound its meter reads on the header\'s line. |\n| `record.section-holds` | A section holds fields, blocks or acts. |\n| `record.sections` | *Refused until adopted or declined:* A register laid out as a table, cards or a gantt states its record\'s `sections` \u2014 the patch holds the fields people write, a files block per paper, a block per child and the record\'s acts. |\n| `record.children` | *Refused until adopted or declined:* An entity some app records, whose rows other rows each belong to (a one-row link to it, never its status history), draws some of them under its row \u2014 a block on its record, a roster of them, lanes of them by that link, or an app over them working in one of its rows at a time (`scope`) \u2014 the patch lists each under the record. |\n| `record.at` | `at` names options of the record\'s stored status, or its milestones; a record with no status has no steps. |\n| `record.at-once` | A stage is the work of one section: two sections naming one option of the status, or one milestone, in `at` hold some field to values apart in their `when` \u2014 a `when` holding the status keeps its section off the options it leaves out. |\n| `record.section-page` | A section is a `page` only on a page door \u2014 never a drawer\'s or one beside the list \u2014 and never a step (`at`), its title naming an address no other page shares (a letter or a digit at least), and never every section. |\n| `record.side` | At most one section stands beside the rest (`side`), on a page door, and never also a page of its own. |\n| `record.beside` | A `door: "beside"` record opens beside a table register \u2014 never a board, cards or a calendar, which draw no list to stand beside. |\n| `record.history` | `record.history` names a status history the entity records (`status.history`), on a record whose Activity is on \u2014 its Activity shows each move, so beside `activity: false` the key draws nothing; no block reads that history. |\n| `record.history-activity` | *Noted, never refused:* `record.history: true` beside a record whose Activity is on: Activity shows every status move as a change already, so the key adds nothing \u2014 a later release refuses it. |\n| `record.section-acts` | A section\'s `acts` name acts of the record itself: an act of a child (`of`) stands on the child\'s row, and one over the register\'s rows (`on`) on the register. |\n| `record.act-in-section` | Every act of the record stands in the `acts` of the section whose work it is; only a correction no section owns \u2014 `danger`, and no other status move offered at the same moment \u2014 or an act an agent runs for the person (a `workflow` or an `intake` moving no status), left off every section, waits in the header\'s \u22EF. |\n| `record.move-home` | A status move offered at one step stands at that step\'s foot when it moves the work forward, to an exit, or back to a step it passes anyway; one offered at several steps, or moving back into a detour, stands in the section `at` the status it enters. |\n| `record.act-staged` | An act at a staged section\'s foot is offered only during that section\'s steps: its `when` holds the status to options the section names \u2014 on milestones, it stamps the step after one of them. |\n| `record.act-hidden` | An act is never offered only while its section\'s `when` hides the section. |\n\n<!-- generated:end rules-record -->\n\nThe record is its `sections`, stacked in the order the work reaches them. A section is\nits fields, the acts under them, then its blocks \u2014 the acts at its foot instead where one\nreads what a block holds or none of its fields \u2014 and holds at least one of them. From\nthree sections on, the record is navigated by their titles: a page by a rail that jumps\nto each (a strip of the same names on a phone), a drawer by tabs;\nwith fewer, the sections just stack. `description` is one sentence under the title.\n`at` names the status options (or the milestones) during which the section is the work\nnow \u2014 only on a record that truly moves through those stages: the rail marks it done,\nnow or waiting (an item of no `at` is its title alone), and the section stands even holding\nnothing yet \u2014 it never hides a field, locks it or gates an act, and a record opens at its\nhead. `page: true` makes a section a page of its own under the\nrecord\'s, off its scroll, with a way back \u2014 rarely, for a long related list or a workspace\nof its own that would make the record worse stacked; never on a drawer or a step. The\nheader draws no act: every act on the record is named by the section whose work it is, and one no section names is refused, naming its likely\nsection \u2014 only a correction no section owns waits in the header\'s \u22EF beside Delete. An act\nmoving the status and offered at ONE step (its `when`) stands at that step\'s foot when it\nmoves the work forward \u2014 to the next step, or to an exit later in the status\'s options\n(Reject beside Approve) \u2014 or back to a step the work passes anyway. One offered at SEVERAL\nsteps, or moving back into a detour, is named by the section it enters: a detour draws it and its\ngaps alone while it waits. A detour \u2014 a stage the work only steps aside into from one step and\nleaves only back to it (a pause, a repair) \u2014 stands before that step, its option and its section\nalike; listed after, the work reads it as its next step, and the check notes it with the move\nlisting it before. A `danger` act is a correction \u2014 waiting in the \u22EF of what it acts\non: its section\'s heading, its row, the header\'s where no section names it \u2014 unless it moves\nthe status while another status move is offered at the same moment; then it is one of that\nstep\'s outcomes, a button. Cancel offered at Held and at Confirmed, beside Confirm, is an\noutcome into an exit: state `{ "title": "Cancelled", "at": ["cancelled"], "fields": ["reason"],\n"acts": ["cancel"] }`. An exit stands only once the record is in it, its reason there and its\nrail item apart from the path; until then Cancel waits in the header\'s \u22EF. Every record\nopens its activity from its head \u2014 each save field by field, each move among them, and each\ncomment, with one composer \u2014 unless `activity: false`. `comments: true` lets people comment\n\u2014 opt-in: state it only where the job\'s people discuss a record; with `activity: false` the\ndoor opens the comments alone, and with neither no door stands. `history: true` draws\nnothing of its own \u2014 each move is already in the activity: noted, refused beside\n`activity: false` (the moves would show nowhere), and refused everywhere in a later release.\n\nA field, a block and an act is each placed ONCE on a record \u2014 the header, one section;\na second place is refused. The header draws the entity\'s `records` title, subtitle,\nimage, status (or milestones) and figure, so none of them is named again in a section\'s\n`fields` or the register\'s `columns` \u2014 save the image, which one `files` block may draw\nagain, whole at reading size (an incident\'s photo, a scan), the header keeping its mark. Refused too: a section holding nothing, an `at`\non a record with no status, and a section\'s act that is a child\'s or runs over the\nregister\'s rows.\nA table, cards or a gantt states its record\'s `sections` (`record.sections`); absent them \u2014 a register laid\nout in time, or one declining the rule \u2014 one section holds every field a person writes that the header does\nnot show. An editable field rests as its control and saves as it changes; everything\nelse is plain text. A section or a block with `when` is shown only while each named\nselect of the record holds one of its options. The one layout serves every entity: a\ncustomer or a product is sections without `at`.\n\n## Blocks\n\n```jsonc\n"blocks": [\n { "rows": "fee", "columns": ["kind", "amount"] },\n { "rows": "fee", "where": { "leg": ["out"] }, "columns": ["kind", "amount"], "under": "invoice" },\n { "rows": "cost", "via": "bill", "create": false, "remove": false, "pick": true },\n { "files": ["photos", "papers"] },\n { "text": "remarks", "when": { "stage": ["held"] } },\n { "statement": [{ "field": "subtotal" }, { "field": "vat", "op": "plus" }, { "field": "total", "op": "equals" }] }\n]\n```\n\n<!-- generated:start apps-blocks -->\n\n#### Rows block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | A child entity whose rows belong to this record |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s fields read as columns after its title \u2014 never what its row draws (subtitle, image, status, figure, or the bound its figure\'s meter reads against), though an `expect` block\'s figure is a column, filled in place. |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the child rows whose single select holds one of these options, or whose yes/no (stored, or a formula) is this value \u2014 every entry ANDed, read so on the server; a row added here holds the selects\' (one leg\'s fees, of a record holding both legs\'). Each select is one the child requires, so no row stands in no block |\n| `expect` | alias | no | The child\'s title when the record holds one row per value of it \u2014 a single select (one per option, in order) or a one-row link to a catalog (one per row the link may point at): a value not yet filled reads as its own empty line, and no value holds two rows. The block\'s `columns` name at least one field a person fills in place, unless it states `post`, whose lines are ticked |\n| `of` | alias | no | With `expect` on a select: the record\'s own multi-select holding the options this record expects a line for \u2014 the options it holds, in order, rather than every option. Its options are among the title\'s |\n| `under` | alias | no | The child\'s one-link to a document that covers its rows (an invoice over its fee lines), whose many-link points back and which links to this record: each document stands as a heading over the lines it covers; a filled line no document covers says so on its own row with a make for one document over it, and two or more waiting take a combined make in the block\'s heading. Never also a rows block of the document |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The child\'s fields the reader narrows the block\'s rows by, as a register\'s `filters` \u2014 none to 3, each a field its rows show (a column, or a part of the row), its status among them where the business narrows by it \u2014 never on a task list, whose status chips narrow it |\n| `search` | list of alias (at least one) | no | The child\'s fields the block\'s search box matches (a link by its row\'s title), a pasted list line by line; absent, the block has no search box |\n| `group` | alias | no | The child\'s field the block\'s rows open grouped by, as a register\'s `group` \u2014 the reader\'s grouping starting there; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order the block\'s rows open in, as a register\'s `sort` \u2014 within each group where the block groups them; absent, the child\'s first deadline soonest first, else the newest first |\n| `edits` | list of alias (1\u20132) | no | The child\'s fields each line holds as its control, written where the line stands (a quote line\'s sell price) \u2014 a number, a date, words or one option the row\'s save writes, each a column or the figure, at most 2; every other value reads, and the row opens to write it |\n| `remove` | `false` | no | This list offers no Delete on its rows (its open row and \u22EF); they are deleted where another list of them or their own record offers it \u2014 e.g. lines that are a selection of rows kept elsewhere |\n| `pick` | `true` | no | The block also picks rows of the child that no record holds through `via` yet \u2014 those naming what this record names in each link `via`\'s `same` compares, and holding the block\'s `where` \u2014 and links them here; a row picked leaves the block from its \u22EF, unlinked and kept. `via` is an optional one-row link a person writes, and the record holds the many-link paired with it |\n| `checklist` | [Checklist](#checklist) | no | The child\'s rows, which are tasks, read as a checklist of checks: each one\'s mark \u2014 passed at a `closed` option, failed at a `failed` one, else still to check \u2014 its title, its columns as its words (a text column its note, on its own line), and on a line under its title its photo and its clause; never grouped nor written in place |\n| `compare` | [Compare](#compare) | no | This list reads each row\'s before against its after (a repair\'s damage and its repair, a check failed and redone): once the before holds a photo, the two stand side by side at one size, each row read in full as a card \u2014 on a checklist, whose `photo` is the before, the after beside it on its line \u2014 and an after not taken yet is the camera\'s tile adding it while the row\'s work is open; a list of the same rows without it stays a table. Never with `expect` |\n| `post` | [Post](#post) | no | This list is a publish desk: with `expect` on a one-row link to a catalog of destinations, every destination the link admits is a line a person ticks \u2014 a tick makes its row from its key alone, an untick deletes it until its status is `closed` \u2014 and the line chosen reads beside the list as the post that destination\'s feed will show, with its words to copy, the block\'s `columns` as its facts and the row\'s acts. Never with `under`, `pick`, `checklist`, `compare` or a `create` list |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Checklist\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photo` | alias | no | The child\'s files field holding the photo each row is checked by |\n| `clause` | alias | no | The child\'s one-row link to what each row is checked against \u2014 a standard\'s clause, a rule |\n| `failed` | list of alias (at least one) | no | Options of the child\'s status a row stands failed at \u2014 none of them `closed` |\n\n#### Compare\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `before` | alias | yes | A files field of the block\'s rows: the photo of the place before the work (the damage surveyed, the check failed) |\n| `after` | alias | yes | Another files field of the block\'s rows: the photo of the same place once the work is done (the repair, the redo) |\n\n#### Files block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `files` | list of alias (at least one) | yes | Files fields of this record, read as pictures and documents |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Text block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `text` | alias | yes | A long text field read as prose |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n<!-- generated:end apps-blocks -->\n\n<!-- generated:start rules-block -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.child-link` | A block reads a child linked to the record; `via` names the link where the child links more than once. |\n| `block.column-drawn` | A child block\'s column never names what the child\'s row draws \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 nor its link back to the record, and names a field once; an `expect` block\'s figure is its filled-in-place column. |\n| `block.title-back` | Where a child\'s title is its link back to the record a block lists it under, its first other subtitle names it there \u2014 a text, a code, a select, a link or a member, never a number or a date. |\n| `block.where` | A rows block\'s `where` names single selects the child requires, by options they hold \u2014 one held at a single option is never also a column \u2014 and the child\'s yes/nos by value. A yes/no is never held: a block adds rows only at a stored one\'s start, and never picks, expects or stands lines under documents. |\n| `block.under` | `under` names the child\'s one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines\' columns never read the document (its heading does) nor hold long text. |\n| `block.under-once` | Documents drawn over their lines (`under`) are never listed again as a block of their own. |\n| `block.edits` | A rows block\'s `edits` name at most two of the child\'s columns or its figure that the row\'s own save writes \u2014 a number, a date, a few words or one option \u2014 never on an `expect` block, whose lines are filled in place already. |\n| `block.remove` | A rows block\'s `remove: false` hides the Delete its rows would offer, so it stands only where the app writes those rows. |\n| `block.compare` | A rows block\'s `compare` names two different files fields of the child: its `before` and its `after` \u2014 never on a block with `expect`, and on a `checklist` its `before` is the check\'s `photo`. |\n| `block.checklist` | A rows block\'s `checklist` reads a task entity\'s rows as checks, one under another \u2014 never expected, under documents, grouped, written in place or picked together by an act over several, its columns no files; its `photo` names a files field of the child, its `clause` a one-row link of the child, and its `failed` options of the child\'s status none of which is `closed`. |\n| `block.pick` | A rows block\'s `pick` links rows of the child through `via` \u2014 an optional one-row link a person writes, no act writes, no other rule compares rows by and no `under` block stands lines under \u2014 paired with a many-link of the record its own save writes; the child\'s `read_scope` reads a row naming no record through `via`; never on an `expect` or `under` block, nor in an app writing only the record\'s children. |\n| `block.narrow` | A rows block\'s `filters`, `search`, `group` and `sort` follow the register\'s rules over the child\'s rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, never on an `expect` block, and an `under` block is grouped by its documents. |\n| `block.expect` | `expect` names the child\'s title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the block; the block\'s `columns` name at least one field a person fills in place \u2014 unless it states `post`, whose lines are ticked \u2014 and its `create` takes the file where the child is pictured by one. |\n| `block.expect-fills` | An expected line is made from its key and its first filled column \u2014 a `post` block\'s from its key alone, by its tick \u2014 so every other field the child requires has a default, a start or a `default_from`, or is read first. |\n| `block.expect-of` | A block\'s `of` needs `expect` on a select, and names the record\'s multi-select whose every option is one of the title\'s. |\n| `block.field-kind` | A files block names files fields of the record, and a text block a text field. |\n| `block.read-scope` | A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history\'s, which takes its record\'s. |\n| `block.under-note` | *Noted, never refused:* Rows holding a paper that cover another block\'s lines, listed apart with no add of their own stated: `under` draws them over the lines they cover. |\n| `block.expect-note` | *Noted, never refused:* Rows each a kind of paper \u2014 a single select their title, a files field holding it \u2014 listed with no `expect` and no `where` on the kind, that no act of the app files a call into or `fills`: the papers a record needs read as the lines still missing. |\n\n<!-- generated:end rules-block -->\n\nAn add in a block fills the link to this record, never asking it, and a rows block opens each row in a drawer.\nUnder `under` the uncovered lines come first; a document made over\none line starts its figure at the line\'s amount, the heading\'s make covering those ticked in its dialog. `pick` (a\nsupplier bill picking the job\'s cost lines) offers none until the record names each link `via`\'s `same` compares,\nlinks at most 200 ticked, and saves the record: a check locking it refuses, and one refusing a row\'s edits refuses\nthat row. Where the child is pictured by a file a person brings (its `records` `image`), an `expect` line not yet\nfilled is a placeholder naming the paper whose upload makes the row; a catalog link\'s values are the rows its\n`options_where` admits, and the add offers only values not yet held. A field starting from the catalog row\n(`default_from`) shows that value as its hint, taken with one press. A block narrowed by a select\'s `where` expects\nits lines once per value among them (one leg\'s fees beside the other\'s), its foot totalled by the record\'s sum\nnarrowed alike. An `agenda` and a `timeline` are set out under \xA7 Timeline and agenda, a `statement` under\n\xA7 Statements.\n\n## Composed page\n\nA record whose page is its own arrangement of what the app already states \u2014 a piece of writing under its title, a\nbrief beside its figures \u2014 states `compose` instead of `sections`: a tree of finished pieces, each named by `use`.\n\n```jsonc\n"record": { "door": "page", "compose": {\n "use": "Column", "children": [\n { "use": "Bar", "children": [\n { "use": "Status" },\n { "use": "Field", "field": "audience" },\n { "use": "Act", "act": "send_for_review" },\n { "use": "Copy", "field": "body", "as": "plain" } ] },\n { "use": "Title" },\n { "use": "Document", "field": "body" },\n { "use": "Group", "title": "Reviews", "children": [\n { "use": "Rows", "child": "review", "create": false },\n { "use": "Act", "act": "ask_reviewer" } ] } ] } }\n```\n\nA node binds by name: a `field` is a field of the record\'s entity, an `act` an act of the app, a `child` an entity\nlinking to the record. What a name binds is read and written exactly as a section naming it would be \u2014 a `Field` and\na `Document` are saved through the record\'s own save, an `Act` runs its workflow, a `Rows` reads the child\'s rows \u2014\nso a tree adds no write of its own. It holds no condition and no computed value: a piece that does not apply on a\nrow (a field its entity\'s `applies` leaves out, an act its `when` does not offer, rows with nothing in them and no\nadd) draws nothing, by the rule that leaves it out of a section, and anything that must depend on the row is an act\'s\n`when` or a field\'s formula, never the tree.\n\nThe first `Act` of the tree is the page\'s primary and every other one secondary; a `danger` act reads so wherever it\nstands. A `Copy` and a `Document` naming one field copy what is on screen \u2014 the words typed and not yet saved among\nthem. The record\'s chrome stays the runtime\'s: the way back, its Activity and its \u22EF with Delete share the row over\nthe tree, and the head draws no title and no status of its own \u2014 the tree\'s `Title` and `Status` do.\n\n<!-- generated:start apps-compose -->\n\n#### Column piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Column"` | yes | The page\'s reading column, 680px: its pieces stacked in order from one left edge |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces stacked in it, in the order they are read |\n\n#### Bar piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Bar"` | yes | One row of small things \u2014 a status, a field\'s control, an act, a copy \u2014 which wraps where it runs out of room |\n| `children` | list of [piece](#composed-page) (at least one) | yes | Its pieces, in order; a `Field` directly in it is its control alone on one line, named by its label |\n\n#### Group piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Group"` | yes | A titled stack, headed once by `title` |\n| `title` | text | yes | Its heading, in the reader\'s words; holding one `Rows`, it carries their count and their add, and the group\'s `Act`s end its line |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces under the heading, in order |\n\n#### Title piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Title"` | yes | The record\'s title (`records.<entity>.title`) as the page\'s heading, written in place where a person writes it; one a page |\n\n#### Status piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Status"` | yes | The record\'s status (`records.<entity>.status`) as a record\'s head draws it: moved by hand, by its acts\' moves, or read |\n\n#### Field piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Field"` | yes | A field of the record under its label, as a section\'s `fields` draws it |\n| `field` | alias | yes | The field drawn \u2014 never the title or the status, which `Title` and `Status` draw; once a page |\n\n#### Document piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Document"` | yes | A markdown text field written in place with no box, saved as it is written \u2014 whether it is saved said at its foot |\n| `field` | alias | yes | A text field with `format: "markdown"`; once a page |\n\n#### Act piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Act"` | yes | An act of the app on the record as its button: its `when`, what it lacks, its asks and its confirmation as everywhere; the first `Act` of the tree is the page\'s primary |\n| `act` | alias | yes | An act of this app on the record \u2014 never one `of` a child\'s rows, which stands on its row; once a page |\n\n#### Rows piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Rows"` | yes | A child entity\'s rows under the record, as a rows block lists them, each row\'s own acts on it |\n| `child` | alias | yes | A child entity linking to the record; once a page |\n| `create` | `false` | no | false, rows are not added here \u2014 an act of the page makes them |\n\n#### Copy piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Copy"` | yes | A button copying a field\'s words as the page holds them now \u2014 what is typed in its `Document` and not yet saved among them |\n| `field` | alias | yes | The text field copied |\n| `as` | `"plain"` \\| `"raw"` | yes | `plain`, its markdown marks gone; `raw`, as stored |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.compose` | A record\'s `compose` is stated instead of `sections`, on a page door, by an app of one book whose records have no steps (`status.milestones`, `status.stages`) and no picture (`image`) \u2014 a composed page draws neither. Each name is a field of the entity, an act of the app on the record or a child entity linked to it; one `Title`, and each field, act and child drawn once \u2014 a field by `Field` or `Document`, never the title or the status, which `Title` and `Status` draw. The first `Act` of the tree is the page\'s next step and every other one secondary; an `Act` is a button alone, never one making a set of papers. A `Field` directly in a `Bar` is its control alone on one line, named by its label; a `Bar` holds no `Rows` and no `Document`. A `Group` is headed once, by its `title`: holding one `Rows`, that heading carries the rows\' count and their add in place of the rows\' own, and the `Act`s the group holds end its line. A `Copy` takes a text field\'s words as the page holds them now, a `Document`\'s words not yet saved among them; a field a `Copy` alone names is read, never written. |\n\n<!-- generated:end apps-compose -->\n\n## Publish desk\n\nRows that are each a place the record is sent \u2014 a notice\'s boards, a listing\'s\nportals \u2014 are a rows block that `expect`s the child\'s one-row link to the catalog of those places and states `post`:\n\n```jsonc\n{ "rows": "posting", "expect": "board", "columns": ["posted_link", "refusal"],\n "post": { "bodies": ["wording", "wording_abroad"], "text": "goes_out_as", "media": "pictures", "link": "page",\n "group": "reach", "network": "kind", "networks": { "company_page": "linkedin" } } }\n```\n\nThe desk is where the post is written. `bodies` are the record\'s plain text fields it is written in (one per\nlanguage, say), each its editor between the list and the preview, saved as any field of the record is and drawn\nnowhere else on it. The preview is `text` read over what is on screen: where `text` is a formula of the child, the\ndesk computes that same formula over what is typed \u2014 a body before it is saved, the line\'s own column \u2014 so the post\nmoves as it is written, and a place not ticked previews the words its row would hold. For that the formula reads only\nwhat a screen holds whole: stored values of its row, the record through lookups over the link back to it, the place\nthrough lookups over `expect`, and formulas of the row over those \u2014 a rollup, a lookup through another link or the\nclock is refused, naming the field.\n\nEvery place the link admits is a line with a tick and that place\'s face: its picture (its `records` `image`), else\nthe mark of the network it is on where `networks` names one, else none. `group`, another single select of the place,\nstands the lines under its options in their order: a kind holding a ticked line is open, one holding none folded to\nits heading and its count \u2014 the first open where nothing is ticked \u2014 and a place of no kind comes last. A tick makes\nthe line\'s row from its key alone \u2014 so nothing else the child requires is left without a default \u2014 and an untick\ndeletes it until the row\'s status is `closed`: what went out is not taken back. It asks first only where a person\nwrote on the row \u2014 its status moved off its first option, or one of the block\'s `columns` a person writes filled. One line is worked beside the list \u2014 the one\npressed, else the first ticked \u2014 and read as the post that place\'s feed shows: `text` is the child\'s words for that\nplace, `media` and `link` the record\'s, and `network` the place\'s single select whose options `networks` names to a\nfeed. A place on an option not named reads its words on a plain card. Under the bodies stand the block\'s `columns` a\nperson writes of the line\'s row (words for that place alone); under the post, the press copying its words beside\nthe acts `of` the child \u2014 never a second primary: the section\'s own act is that \u2014 then what the row\'s line and the\nblock\'s `columns` hold that nobody writes there (the link it went out at, why it was refused), each drawn only once\nit holds a value. A value an act or a workflow fills is stated as that act\'s, so the desk never offers it as an\nempty box. Where the block\'s box seats three columns they are the list, what is written and\nthe post; narrower, what is written stands over the post; narrower still the list stands alone and a line opens in\nplace. Sending is an\nordinary act of the record, named in the section\'s `acts`.\n\n<!-- generated:start apps-post -->\n\n#### Post\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `bodies` | list of alias (1\u20133) | no | The record\'s plain text fields the post is written in, in order \u2014 one per language, say \u2014 at most 3: each its editor on the desk, between the list and the preview, and drawn nowhere else on the record |\n| `text` | alias | yes | A text of the block\'s rows \u2014 stored, or a formula yielding text \u2014 holding the words that go out on that row\'s destination. A formula is computed on the desk too, over what is typed: it reads stored values of its row, the record through lookups over the link back to it, the destination through lookups over `expect`, and formulas of the row over those \u2014 never a rollup, another link or the clock |\n| `media` | alias | yes | A files field of the record: the pictures or the video the post carries |\n| `link` | alias | no | A text field of the record holding a link (`format: "link"`), carried as the post\'s card |\n| `network` | alias | yes | A single select of the entity `expect` links to, saying which network each destination is on |\n| `group` | alias | no | Another single select of the entity `expect` links to \u2014 the destination\'s kind \u2014 the lines stand grouped by: each option a heading in the select\'s order, a group holding no ticked line folded to its heading and its count, destinations holding no option last |\n| `networks` | map of alias \u2192 `"facebook"` \\| `"instagram"` \\| `"threads"` \\| `"x"` \\| `"linkedin"` | yes | That select\'s options, each by its alias, to the network whose feed the post is previewed in \u2014 facebook, instagram, threads, x, linkedin. An option not named has no feed to imitate: its words read as a plain card |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.post` | A rows block\'s `post` needs `expect` on a one-row link \u2014 never a select, nor with `under`, `pick`, `checklist`, `compare` or a `create` list, its line made by a tick from its key alone: its `bodies` are plain text fields of the record a person writes, drawn nowhere else on it; its `text` is a text of the child, stored or a formula yielding text \u2014 a formula reading only stored values of its row, lookups through the link back to the record or through `expect`, and formulas of the row over those, never the clock; its `media` a files field of the record and its `link` a text field of the record; its `network` a single select of the entity the link names, and `networks` holds options of that select, by alias, each to one of facebook, instagram, threads, x, linkedin; its `group` is another single select of that entity. |\n\n<!-- generated:end apps-post -->\n\n## Timeline and agenda\n\nTwo blocks read a child\'s rows by their day: a `timeline` logs what happened, newest first; an `agenda` plans\nwhat is to come on a time axis.\n\n```jsonc\n"blocks": [\n { "timeline": "note" },\n { "agenda": "booking", "columns": ["guide"], "start": "arrival" }\n]\n```\n\n<!-- generated:start apps-timeline-agenda -->\n\n#### Timeline block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `timeline` | alias | yes | A child entity read as a log of dated entries, newest first, with a composer where the app adds entries, each corrected and removed where it stands as a rows block\'s row (entries planned ahead are an `agenda`) \u2014 never the record\'s status history: its Activity shows each move |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, the subtitle and figure no default fills, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Agenda block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `agenda` | alias | yes | A child entity read as planned entries on a time axis: by day, soonest first, each entry with its picture and the day that is today marked. The day is the first date on the child\'s row line (its `records` title or subtitle); within a day entries follow a datetime there, else the first single select after the date \u2014 its options in the day\'s order |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s facts read on an entry\'s line after its title \u2014 never what its line already draws (subtitle, image, status, figure) |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `start` | alias | no | The record\'s date that is the first day: each day\'s heading counts from it (Day 1, Day 2); absent, the weekday and the date alone |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.timeline` | A timeline\'s child holds a date to place its entries by. |\n| `block.agenda` | An agenda\'s child names a date on its row line (`records` title or subtitle); `start` is a date of the record. |\n| `block.timeline-note` | *Noted, never refused:* Rows each what happened on a day and who \u2014 a date on their line, a member, no status and no figure \u2014 listed as a table: a `timeline` draws them as a log, newest first. |\n| `block.agenda-note` | *Noted, never refused:* Rows each on a day and within it (at an hour, in a part of the day) listed as a table: an `agenda` draws them by day. |\n\n<!-- generated:end apps-timeline-agenda -->\n\nAn entry with no day yet is a plan, listed first and dated in place; a timeline\'s moment is its child\'s first date,\nand who its member field.\n\n## Statements\n\nA `statement` block draws a figure the record works out from its own figures \u2014 a claim\'s payable, an invoice\'s\ntotal, a payslip\'s net \u2014 line by line in the order the arithmetic runs: each line a number of the record, the first\nstating no `op`, the last an `equals`. Every figure is a field the server computes or a person writes; the screen adds\nnothing up. A line\'s `parts` lists the rows its figure carries, each read from its own row, and that figure is what\nthey add up to \u2014 a formula, rollup or lookup, or the server\'s copy when the record is made (`default_from`), which\nnothing writes after. A claim\'s "certified before" is the contract\'s certified total copied when the claim is made\n(`made_at`, a moment the record `starts` at `today`), over the claims certified (`certified_at`, an act\'s `now`)\nbefore that moment, each at the figure its certifying act froze (`certified_value`, set to\n`{ "formula": "{claim_value}" }`, which the contract\'s total sums) \u2014 so they always add up to it, as a signed\ncertificate does, after a percent is edited or the contract varied.\n\n```jsonc\n{ "statement": [\n { "field": "contract_value" },\n { "field": "done_to_date", "op": "times" },\n { "field": "value_to_date", "op": "equals" },\n { "field": "certified_before", "op": "minus",\n "parts": { "rows": "claim", "same": "contract", "at": "certified_at", "before": "made_at", "where": { "kind": ["work"] },\n "columns": ["claimed_on", "done_to_date"], "figure": "certified_value" } },\n { "field": "claim_value", "op": "equals" },\n { "field": "retention", "op": "minus", "rate": "retention_rate" },\n { "field": "payable", "op": "equals" }\n ],\n "when": { "kind": ["work"] } }\n```\n\n<!-- generated:start apps-statement -->\n\n#### Statement block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `statement` | list of [Statement line](#statement-line) (at least 3) | yes | A figure built from figures of the record, read as a statement: each line its operator, its label and its figure, the figures on one right edge, in the order stated \u2014 the last an `equals`, what it all comes to, drawn heaviest (a claim\'s payable, an invoice\'s total, a payslip\'s net). A line\'s figure is a control where the app writes it |\n| `title` | text | no | The block\'s heading; absent, the label of what the statement comes to |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Statement line\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number of the record \u2014 stored, a formula, a rollup or a lookup \u2014 read on the line after its label |\n| `op` | `"plus"` \\| `"minus"` \\| `"times"` \\| `"equals"` | no | How the line works on the lines above it \u2014 `plus`, `minus`, `times`, or `equals` for what they come to, a formula, rollup or lookup the server computes; absent on the first line, which the rest work from |\n| `rate` | alias | no | A percent of the record read after the line\'s label: the rate its figure is taken at (retention at 5 %) |\n| `parts` | [Statement parts](#statement-parts) | no | The rows the line\'s figure carries, each listed under it with its words and its own figure \u2014 read from those rows, never a copy of them. The line\'s figure is what they add up to, drawn read: a formula, rollup or lookup, or a value the server copies when the record is made (`default_from`) that nothing writes after |\n\n#### Statement parts\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows the line\'s figure carries \u2014 the record\'s own for the earlier rows of the same thing |\n| `same` | alias | yes | A one-link the record and those rows both hold: the rows naming what the record names in it |\n| `at` | alias | no | A moment (a datetime) of those rows no person keys \u2014 when each was certified: with `before`, only the rows whose moment stands before the record\'s, earliest first; absent with it, every row so named, in their own order |\n| `before` | alias | no | A moment (a datetime) of the record no person keys \u2014 when it was made: the moment the rows\' `at` stands before; stated with `at` |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the rows whose single select holds one of these options, or whose yes/no (stored, or a formula) is this value \u2014 read so on the server |\n| `columns` | list of alias (1\u20132) | yes | The rows\' fields read as each one\'s words, in order (its date, its percent) |\n| `figure` | alias | yes | The rows\' number each one reads at the figures\' edge, frozen when the row was certified: a stored number an act fills (`{ "formula": \u2026 }` as it runs) or the server copies when the row is made (`default_from`) \u2014 never one a person keys, nor a formula, rollup or lookup the server recomputes |\n\n<!-- generated:end apps-statement -->\n\n<!-- generated:start rules-statement -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `statement.lines` | A statement\'s lines are numbers of the record, each once \u2014 the first stating no `op`, every other one, the last an `equals` \u2014 each `equals` a formula, rollup or lookup, and a `rate` a number of the record. |\n| `statement.rows` | A line\'s `parts` read rows of an entity holding `same`, a one-link of the record\'s to the same entity; `at`, a moment of those rows, and `before`, a moment of the record (datetimes, never days), stated together; their `columns` and `figure` are those rows\' fields, the figure a number. |\n| `statement.parts-keyed` | A line\'s `parts` add up to its figure, so no person keys it: a formula, rollup or lookup, a value the server copies when the record is made (`default_from`) that no add asks and no save or act writes, or a stored number stating `writes: false` \u2014 the line draws it read; each part\'s `figure` is frozen when its row was certified \u2014 a stored number an act fills, the server copies or the model states no person keys (`writes: false`), never one a person keys or the server recomputes \u2014 and no person keys the moments `at` and `before` either. |\n\n<!-- generated:end rules-statement -->\n\n## Reading blocks\n\nA `metric`, `breakdown`, `trend`, `pivot` or `progress` block on a record is a reading (\xA7 Readings) of a child\nentity\'s rows under it, read only \u2014 `via` names the link where the child has more than one.\n\nA `figures` block reads the record\'s own numbers instead, each a tile under its field\'s label: two to eight\nnumbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number\nstating `writes: false` (what a workflow or an integration writes). A number a person keys is a field of the\nsection, where it is written. A tile holding nothing reads as not measured, never 0 \u2014 a sum or a count of\nnothing yet reads 0; a stored 0 reads 0.\n\nMetric blocks and a `figures` block standing one straight after another in a section draw as one strip: four\ntiles to a line on a desk, two on a phone.\n\nA `funnel` block reads the same rows as steps people drop out of between: each of its `steps` a number column\nof the child, summed over the rows under the record, drawn in order with the share each keeps of the one before.\nFunnel blocks standing one straight after another in a section draw on one grid \u2014 one name column, one bar origin\nand one value column \u2014 each under its own `label`, its bars over its own first step.\n\n```jsonc\n"blocks": [\n { "metric": "fee", "value": "amount", "label": "Fees" },\n { "breakdown": "fee", "by": "kind" },\n { "trend": "fee", "over": "charged_on", "value": "amount" },\n { "pivot": "fee", "rows": "kind", "columns": "method" },\n { "progress": "task", "planned": { "start": "plan_start", "end": "plan_end" }, "done": "complete", "label": "Progress" },\n { "funnel": "channel_week", "steps": ["impressions", "clicks", "bookings"], "label": "Reach to booking" },\n { "figures": ["sessions", "bookings", "calls", "ad_spend"] }\n]\n```\n\n<!-- generated:start apps-reading-blocks -->\n\n#### Metric block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Breakdown block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Trend block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Pivot block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side |\n| `columns` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Progress block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `progress` | alias | yes | The entity whose rows are the parts of the work, read as the share of it planned and the share done, day by day |\n| `planned` | [Progress planned](#progress-planned) | yes | The days each row\'s work is planned over, its share planned spread evenly across them \u2014 a row missing either is no part of the plan, and neither line counts it |\n| `done` | alias | yes | A number of the row the server computes: its share done today, in percent (0\u2013100) |\n| `finished` | alias | no | A date of the row: the day its work was finished, from which it stands at its `done` |\n| `measured` | [Progress measured](#progress-measured) | no | Before a row is finished, its share done on a day is what these add up to by then, never past its `done`; absent, a row stands at nothing until it is finished, and at its `done` today |\n| `weight` | alias | no | A number of the row: what it weighs in the whole \u2014 its value, its share of the contract; a row holding none weighs nothing. Absent, each row weighs its planned days |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Funnel block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `funnel` | alias | yes | A child entity whose rows each count the same steps, a column a step (a week\'s channels, each with its impressions, clicks and bookings): one funnel over the rows under the record \u2014 each step its column summed, its bar over the first step, the share kept between two steps and from the first to the last. A step no row holds a number in reads as not measured, never as zero |\n| `steps` | list of alias (2\u20138) | yes | The child\'s numbers summed, in order, first step to last \u2014 2 to 8, each stored, a formula or a rollup, all in one notation, never a percent |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Figures block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `figures` | list of alias (2\u20138) | yes | Numbers of this record no person keys, in order, each a tile under its field\'s label \u2014 2 to 8: a formula, a rollup, a lookup, or a stored number stating `writes: false`. A tile holding nothing reads as not measured, never 0 \u2014 a sum or a count of nothing yet reads 0 |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.funnel` | A funnel\'s `steps` are 2 to 8 numbers of its child, each named once \u2014 stored, a formula or a rollup \u2014 all in one notation, never a percent nor a unit read on each row. |\n| `block.figures` | A `figures` block\'s tiles are 2 to 8 numbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number stating `writes: false` \u2014 each placed once on the record. |\n\n<!-- generated:end apps-reading-blocks -->\n\n## Tasks\n\nAn entity whose `records` states `"task": true` \u2014 its status a stored select with `closed` \u2014 is\nwork someone finishes. Every table of its rows draws it alike \u2014 a rows block of the record it\nbelongs to, its own register, the rows filed under one of its rows (its steps) \u2014 each row led by a\nring. Ticking the ring moves the row to a closed option by the one write the app already has for\nthat move \u2014 the act of that entity setting a closed option (its first preferred), offered on an\nopen row and asking nothing, else its own save where people write the status \u2014 stamping what\nthat act stamps and appending the status history. Unticking runs the act moving a closed row\nback (its `when` holding a closed option), else the save back to the status\'s default. A move no\nwrite reaches leaves the ring disabled, never hidden, and the check notes it. The status is moved\nin place on the row by the same moves; every other field is edited where the row opens. A\nfiles column reads on a task\'s line as how many files it holds. An act `of` the child with\n`on: "rows"` completes several at once: the list\'s heading offers to select rows, and the act\nruns on those picked. A block with `expect` or `under` draws its own lines, never rings. A record listing rows of a\ntask entity filed under it by a link holding one row says how much of that work is done \u2014 those rows at a closed\noption of all, every one the reader may read, every such block\'s rows counted as one \u2014 wherever its row is drawn: a\nmeter on its head, a column of its register\'s rows, a card\'s foot, a calendar entry, a gantt\'s or a lane\'s bar; a\ngantt of task rows grouped by the row they are filed under says that row\'s on each group\'s head. No rollup is stated\nfor it.\n\n```json\n{\n "entities": [\n {\n "alias": "project",\n "label": "Projects",\n "singular": "Project",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "lead", "label": "Lead", "type": "select_member" }\n ]\n },\n {\n "alias": "task",\n "label": "Tasks",\n "singular": "Task",\n "fields": [\n { "alias": "title", "label": "Title", "type": "text", "required": true },\n {\n "alias": "project",\n "label": "Project",\n "type": "select_record_link",\n "target_entity": "project",\n "cardinality": "one",\n "required": true\n },\n { "alias": "assignee", "label": "Assignee", "type": "select_member" },\n {\n "alias": "status",\n "label": "Status",\n "type": "select",\n "required": true,\n "options": [\n { "alias": "todo", "label": "To do", "color": "slate" },\n { "alias": "done", "label": "Done", "color": "green" }\n ],\n "default": ["todo"]\n },\n { "alias": "due", "label": "Due", "type": "date", "format": "date" },\n { "alias": "done_on", "label": "Done on", "type": "date", "format": "date" }\n ]\n }\n ],\n "records": {\n "project": { "title": "name" },\n "task": {\n "title": "title",\n "subtitle": ["assignee"],\n "status": { "field": "status", "closed": ["done"] },\n "due": ["due"],\n "task": true,\n "starts": { "assignee": "me" }\n }\n },\n "apps": [\n {\n "alias": "projects",\n "name": "Projects",\n "entity": "project",\n "record": {\n "sections": [{ "title": "Work", "fields": ["lead"], "blocks": [{ "rows": "task", "columns": ["due"] }] }]\n },\n "acts": [\n { "alias": "complete", "label": "Complete", "of": "task", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "reopen", "label": "Reopen", "of": "task", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } },\n { "alias": "complete_all", "label": "Complete", "of": "task", "on": "rows", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } }\n ]\n },\n {\n "alias": "my_tasks",\n "name": "My tasks",\n "entity": "task",\n "register": {\n "opens": { "assignee": "me" },\n "readings": [{ "metric": "task", "where": { "status": ["todo"] }, "label": "Open" }]\n },\n "record": {\n "door": "drawer",\n "sections": [{ "title": "Task", "fields": ["project", "due"], "acts": ["finish", "undo"] }]\n },\n "acts": [\n { "alias": "finish", "label": "Finish", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "undo", "label": "Undo", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } }\n ]\n }\n ]\n}\n```\n\n## Acts\n\n```jsonc\n"acts": [{\n "alias": "gate_out", "label": "Gate out",\n "when": { "stage": ["in_yard"] }, "requires": ["release", "seal"],\n "asks": ["seal", { "input": "note", "label": "Note", "type": "long_text" }],\n "set": { "stage": "gone", "left_on": "now", "left_by": "me", "remark": "input:note" },\n "confirm": true\n}]\n```\n\n<!-- generated:start apps-acts -->\n\n#### Act\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The act\'s name within its app \u2014 its workflow is `act_<alias>` |\n| `label` | text | yes | The verb, as the reader says it |\n| `on` | `"record"` \\| `"rows"` \\| `"view"` | no | `rows` runs on the rows ticked in the register \u2014 with `of`, on the child rows ticked together in a rows block of the record; `view` on every row the register shows (offered while the view is narrowed to its `when`; it states no `requires`); absent, on one record (its page and its row\'s menu) |\n| `of` | alias | no | A rows block\'s child entity the act works on: offered on each child row \u2014 or, `on: "rows"`, on the rows ticked together in the block \u2014 its conditions and its write that row\'s |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The act works only while each named select holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `requires` | list of alias (at least one) | no | Fields that must be filled first, each drawn where the act stands (its section\'s fields, a block\'s, or the act\'s asks \u2014 never the header alone); a blocked act names each one missing |\n| `recommends` | list of alias (at least one) | no | Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank |\n| `asks` | list of (alias \\| [Asked input](#asked-input)) (at least one) | no | What the reader states when pressing it \u2014 fields of the record (written by the act) or inputs |\n| `set` | map of alias \u2192 (text \\| number \\| boolean \\| `null` \\| [Set formula](#set-formula)) | no | What the act writes: an option alias, a value, `"now"`, `"me"` or `"input:<name>"` \u2014 each the act\'s outcome, read-only elsewhere; or `{ "formula": \u2026 }`, computed over each acted row as it stands when the act runs and written then (a sell price at cost and markup): a starting value, which a person may change after and the act never recomputes |\n| `workflow` | alias | no | An authored workflow (src/workflows/<alias>.ts) the act runs after its checks, for what `set` cannot say |\n| `writes` | list of alias (at least one) | no | With `workflow`, fields of the acted row its body writes (an id kept from a service\'s answer) \u2014 the act\'s, as what `set` states is |\n| `template` | alias | no | A document template the act makes a file from \u2014 an html one made a PDF, an excel one a workbook; over several rows, one file of them all, the rows listed under the entity\'s alias |\n| `templates` | list of [Act template](#act-template) (at least 2) | no | Several papers the act makes into `into` (a case\'s forms), instead of one `template`: listed where the act stands \u2014 each made one as its file, each not yet made as a placeholder \u2014 the reader ticks which to make, one press making those, each replacing its own last paper |\n| `into` | alias | no | The record\'s files field the made document is kept in \u2014 remade, it replaces its own last paper, never a person\'s file |\n| `paper` | text | no | Its paper, `<child>.<kind>`: the line of the record\'s papers it stands on |\n| `intake` | alias | no | A child the record lists in a rows block whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child \u2014 its kind the child\'s title, its file in the child\'s files field |\n| `record` | [Recording](#recording) | no | The press records a call, visit or meeting through the host \u2014 Stop ends it \u2014 and files it as a new row of a child the record lists: its audio, its screen where captured, its transcript |\n| `sign` | [Signature](#signature) | no | The press opens a pad the signer signs on, beside what the act asks, and the act keeps the signature as a picture with who signed, when and the witness \u2014 read on the record as one signature, and handed to the paper the act makes |\n| `fills` | list of text (at least one) | no | With `intake`, what the papers read may fill: a field of the record, `<link>.<field>` on the row a required one-row link names, or a child whose rows the papers add \u2014 shown before and after, saved where it differs; a single link is filled with a row the app reads. With `record`, fields of the new row an agent fills from the transcript, saved as they come |\n| `confirm` | `true` | no | The press asks first; the question lists every active warning |\n| `danger` | `true` | no | The act destroys or cannot be undone |\n\n#### Asked input\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `input` | alias | yes | A value the act asks for that is not a field of the record |\n| `label` | text | yes | What the reader is asked, in their words |\n| `type` | `"text"` \\| `"long_text"` \\| `"number"` \\| `"date"` \\| `"select"` \\| `"link"` \\| `"member"` | yes | What the reader states |\n| `options` | list of [Select option](#select-option) (at least one) | no | The choices a `select` input offers, one picked \u2014 written into a select holding the same option aliases |\n| `entity` | alias | no | The entity a `link` input picks one row of |\n| `required` | `true` | no | The act is refused without it |\n| `default` | alias | no | A field of the row whose value the input starts from, still changed at will |\n\n#### Set formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | text | yes | A formula over the acted row\'s fields, `{alias}` each, read as the entity\'s own formulas read them |\n\n<!-- generated:end apps-acts -->\n\n<!-- generated:start rules-act -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.paper-note` | *Noted, never refused:* A made paper kept in a field of a record that lists its papers by kind: it is one of those lines. |\n| `act.moves` | *Refused until adopted or declined:* Rows a stored status stages towards `closed` options are moved by an act of some app over them, or by its authored `workflow` \u2014 the patch moves each open option to the next, the last to the first closed one. |\n| `act.set-not-asked` | A field an act `set`s \u2014 an option, a value, `now`, `me`, an input \u2014 or its workflow `writes` is the act\'s outcome: read-only everywhere else and never asked by an add (`create`); a required one takes a `default`. A field an act computes by a `formula` is a starting value, which a person may change after. |\n| `act.writes` | `writes` stands beside `workflow` and names stored fields of the acted row, each once, that no `set` of the act and no other act over the rows states. |\n| `act.set-formula` | `set: { <field>: { formula } }` reads fields of the acted row and computes what the field holds \u2014 a number, a date, text or a yes/no, typed as the entity\'s own formulas are \u2014 never the status or a milestone; a bound a write rule holds the field to is checked on the computed value as the act runs. |\n| `act.of` | An act\'s `of` names a child the record lists in a rows block; it works on the one child row it is pressed on \u2014 or, `on: "rows"`, on the rows ticked together where the record lists that child as tasks \u2014 never on the rows in view. |\n| `act.writes-line-key` | An act never writes the link or the key naming an expected line under its record \u2014 a line\'s key is stated by filling it. |\n| `act.makes-or-runs` | An act makes a document (`template`, `templates`) or runs an authored `workflow`, never both \u2014 the workflow makes what it needs. |\n| `act.view` | An act over every row in view (`on: "view"`) writes (`set`), makes a document or runs a workflow, and states no `requires`. |\n| `act.requires-drawn` | Every field an act `requires` is drawn where the act stands \u2014 its section\'s fields, a block\'s, its asks, or an unstaged section\'s where its own is unstaged \u2014 never the header alone. |\n| `act.requires-written` | A field an act requires that only an act writes is written by an act offered together with it, or asked by the act itself. |\n| `act.recommends` | A field an act `recommends` is not also in its `requires`. |\n| `act.set` | `set` writes each field a value it holds \u2014 an option alias, a number, a yes/no, a text, `"now"` on a date, `"me"` on a member, `"input:<name>"` of an input the act asks of the same kind \u2014 or clears an optional one with `null`. |\n| `act.set-milestone` | An act stamps a milestone, never clears one \u2014 unticking clears it and every later one. |\n| `act.paper` | `paper` is a kind the record lists its papers by, on one record \u2014 never beside `into` or `templates`; one maker per kind. |\n| `act.history-copy` | What a status move asks is copied onto the history\'s field of the same name, which holds the same kind of value. |\n| `act.input` | An input has a name of its own \u2014 never another ask\'s, a field\'s, or the record input its workflow takes \u2014 `options` exactly when it is a select, `entity` exactly when it is a link, and a `default` field holding what it states. |\n\n<!-- generated:end rules-act -->\n\nAn act whose `when` does not hold is not offered \u2014 each select named holding one of its\noptions, each yes/no named (stored, or a formula: a period that has ended) its `true` or\n`false`; every one that holds stands at the\nfoot of the section naming it in `acts` \u2014 a correction in that section heading\'s \u22EF, one\nof a child row on its row (a correction in the row\'s \u22EF). An act\'s `requires` are fields\nits section draws \u2014 its `fields`, a block\'s, the act\'s `asks`, or those of another section\nwith no `at` where its own has none; one shown only in the header or a staged section\nelsewhere is refused. One whose `requires` are\nempty is offered disabled, naming each; one whose `recommends` are empty names what\nit will leave blank and still presses; one a standing check blocks is offered\ndisabled in the check\'s words. The generated `act_<alias>` workflow re-checks all three on the\nserver, writes `set` and what was asked, and appends the status history where the\nact moves the status. A field an act asks is read-only everywhere else. A `select` input\noffers its own options and is written into a select holding the same option aliases; a `link` input picks one row of its `entity`\nand a `member` input one person, each written into a field of its kind. A field an act\nboth `requires` and `asks` never disables the press \u2014 the act asks it, required.\n\n`on: "view"` runs on every row the register shows \u2014 its search, status and filters \u2014\neach row checked before any is written. `of` names a rows block\'s child entity: the\nact is offered on each child row (its menu and its drawer), its `when`, `requires`,\n`set` and checks are the child\'s, it runs on that row, and a check of the record that\nlocks the child rows locks it too. With `on: "rows"` it runs on the child rows ticked\ntogether in the record\'s task list (\xA7 Tasks) \u2014 every row checked before any is written;\nit is refused where the record lists no tasks of that child.\n\nWhat an act makes, reads, records or has signed is set out under \xA7 Papers and recordings.\n\n## Papers and recordings\n\nBeside what it writes, an act makes papers from templates (`template`, `templates`), reads papers brought into the\nrecord (`intake`), records a call or a visit (`record`) and takes a signature on the device (`sign`).\n\n<!-- generated:start apps-papers -->\n\n#### Act template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `template` | alias | yes | A document template the act makes a paper of, kept in `into` \u2014 an html one made a PDF, an excel one a workbook |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked |\n\n#### Recording\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | A child the record lists in a timeline or rows block, linking back to it by a single link: each recording is filed as a new row of it |\n| `audio` | alias | yes | The child\'s files field the recording\'s audio is kept in |\n| `transcript` | alias | yes | The child\'s text field the transcript is kept in, a speaker\'s turn per line |\n| `video` | alias | no | The child\'s files field the screen is kept in, where the recording captured it |\n| `at` | alias | no | The child\'s date field stamped with the moment the recording started |\n| `by` | alias | no | The child\'s member field set to the person who recorded |\n\n#### Signature\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | The record\'s files field the signature is kept in, as a picture \u2014 each signing replaces the one before |\n| `by` | alias | yes | Who signed: a text field the signer types their name into (someone with no seat in the workspace \u2014 a driver, the owner\'s supervisor), or a member field set to the person pressing, who signs as themselves |\n| `at` | alias | yes | The record\'s date field stamped with the server\'s time as the act runs |\n| `witness` | alias | no | Beside a text `by`, the member field set to the person pressing, who saw the signer sign |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.into` | `into` is a files field of the one record the act is pressed on, beside its `template` or `templates`. |\n| `act.templates` | `templates` names each template once, kept in `into`, on one record pressed alone \u2014 never beside `template`, `asks`, `of` or an `on` over several rows. |\n| `act.intake` | `intake` names a child the record lists in a rows block that expects its title \u2014 a single select, the paper\'s kind \u2014 and is narrowed by no `where`, holding exactly one files field and needing nothing an added line does not fill; the act runs on one record, states `fills`, and states nothing but its `label`, `when` and `requires` beside them. |\n| `act.record` | `record` names a child the record draws in a timeline or a rows block with no `expect`, `under` or `where`, linking back by one single link, its row needing nothing the recording does not write: `audio` and `video` files fields, `transcript` text, `at` a date, `by` a member, each its own field; on one record, stating only `label`, `when`, `requires` and `fills` beside it. |\n| `act.fills` | `fills` names each once, only beside `intake` or `record`. Beside `record`: a field of the recording\'s row the recording does not write, never a link. Beside `intake`: a field of the record, `<link>.<field>` on the row a required one-row link names (never beside that link), or a child the record adds rows of in a rows block with no `expect`, `under` or `where`, filled in the fields that block adds \u2014 every one a new row needs among them. Each field filled is one a person writes \u2014 text, a number, a date, a yes/no, options or a single link\'s row \u2014 never a status, a milestone, a field an act writes or a link `same` narrows; a link\'s row is one the app reads of its entity. |\n| `act.fills-drawn` | *Noted, never refused:* A field `fills` writes that the record draws nowhere: the review shows the change, the record does not. |\n| `act.fills-new` | *Noted, never refused:* A child `fills` adds rows of, whose natural key the papers cannot state: every row read is added, so the same papers read twice list them twice. |\n| `act.sign` | `sign` runs on one record, never a child\'s row: `into` a files field, `by` a text field the signer types or a member field set to the person pressing, `at` a date, `witness` a member field \u2014 stated exactly beside a text `by` \u2014 each a field of its own that the act does not also ask, set or `writes`, and no other act signs into `into`. |\n\n<!-- generated:end apps-papers -->\n\n`template` makes a document from the row as the act left it and\nkeeps it in `into`; over several rows it makes one document of them all, the rows\nlisted under the entity\'s alias, and hands it back. `templates` instead names a set of\npapers kept in `into`, listed where the act stands \u2014 each made one as its file, each\nnot made yet as a placeholder \u2014 those whose `when` holds ticked to start; the press\nmakes the ticked ones (sent in `templates`), each replacing the file its template made\nbefore (the file whose `document_template_id` is that template \u2014 never a file a person\nput there), and the set downloads as one archive (`archive_<alias>`).\n\n`intake` reads papers: the press takes files, an agent (`act_<alias>` among the app\'s\nagents) reads them, and each is filed as the record\'s line of its kind in the named\nchild \u2014 that child\'s title \u2014 or a new line, its file in the child\'s one files field.\nWhat they state of each field `fills` names is shown beside what the row holds; the save\nwrites only what differs \u2014 the record\'s fields, `<link>.<field>` on the row a required\none-row link names, a child\'s rows (one found by the child\'s `natural_key` changed, else\nadded) \u2014 each refused as its own save is, and nothing written while one refuses. A single\nlink among them is filled with a row the agent finds through the app\'s read of the rows\nit may name \u2014 narrowed as its picker is \u2014 and an id naming no row of its entity is refused\nbefore the run ends; a link `same` narrows is not filled.\n\n```jsonc\n{ "alias": "read_papers", "label": "Read papers", "intake": "customer_paper", "fills": ["phone", "contact.email", "branch"] }\n```\n\n`sign` takes a signature on the device in front of the signer: the press opens a pad in a\nsheet with what the act asks \u2014 the signer\'s name, where `by` is a text field they type it\ninto \u2014 and the act keeps the picture in `into` in place of any before it, `at` stamped with\nthe server\'s time and the person pressing in `witness` (a member `by` is the person\npressing, signing as themselves). The record reads `into` as one signature \u2014 the stroke,\nwho signed, as what, when and before whom \u2014 and the act\'s paper is handed the picture under\n`into`\'s name, an html template printing it by its url (`{{#each signature}}<img src="{{url}}">{{/each}}`).\n\n```jsonc\n{ "alias": "gate_in", "label": "Make the gate-in EIR", "template": "eir_in", "into": "eir",\n "sign": { "into": "driver_signature", "by": "driver_name", "at": "signed_at", "witness": "signed_before" } }\n```\n\n## Checks\n\n```jsonc\n"checks": [{ "field": "release_warning" }, { "field": "unpaid", "blocks": ["gate_out"] }, { "field": "locked", "blocks": ["edit"] }]\n```\n\n<!-- generated:start apps-checks -->\n\n#### Check\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A yes/no or text formula of the record (or of the child `of` names): true or non-empty means the check stands; its label is its short name on a row\'s line, and a text formula\'s words are the sentence the record and a tooltip say |\n| `of` | alias | no | A rows block\'s child entity this check is a formula of: standing on a child row, it refuses what it blocks of that row |\n| `blocks` | `"all"` \\| list of (alias \\| `"edit"` \\| `"delete"`) (at least one) | no | Acts, "edit" (the saves and the Delete), "delete" (the Delete alone) or "all" that the standing check refuses; absent, it only warns |\n| `resolve` | alias | no | An act of this app, on the rows the check stands on, that clears it: the check\'s line links to the act where it stands, and the act keeps its own conditions |\n\n<!-- generated:end apps-checks -->\n\n<!-- generated:start rules-check -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `check.field` | A check\'s `field` is a yes/no or text formula of the record, or of the child `of` names. |\n| `check.of` | A check\'s `of` names a child the record lists in a rows block. |\n| `check.not-placed` | A check\'s field is never also placed on the record \u2014 the line under the header says it. |\n| `check.blocks` | A check of the record blocks its acts, `"edit"`, `"delete"` or `"all"`; a check of a child blocks that row\'s saves, its Delete, the acts `of` that child, or all \u2014 never the record\'s acts. |\n| `check.resolve` | `resolve` names an act of this app on the rows the check stands on, pressed on one row, not refused by the check, and drawn as a button \u2014 never a correction. |\n| `check.restates-meter` | A warning reading a gated meter\'s figure against its bound or pass mark is refused \u2014 the meter draws it. |\n\n<!-- generated:end rules-check -->\n\nA check is a yes/no or text formula of the record: it stands while true or\nnon-empty, and a text formula\'s words are what the reader sees. Without `blocks`\nit warns; with them it refuses the acts named, `"edit"` the record\'s own saves (and\nits Delete), `"delete"` its Delete alone, or `"all"` \u2014 on the server, in every write it\nnames. A check `of` a rows block\'s child stands on each child row: it refuses that\nrow\'s saves (`edit`), its Delete (`delete`), the acts `of` that child it names, or\n`all` of them \u2014 never the record\'s acts. A warning reading the figure of a meter that\n`gates` marks against its bound or its pass mark is refused: the meter already draws it.\n\nThe check field\'s `label` is its short name \u2014 "Over budget", "Release expired": a row\'s\nline says it in the check\'s tone (the most severe of several, and how many more), while\nits words whole are the row\'s tooltip and stand under the record\'s header. So label it\nas the reader names the problem, in a few words, and let a text formula say the\nsentence. The compiler records the fields a formula reads: a row drawing each of them\nas a control says nothing of the check on its line (the empty control says it), and a\ncheck with no `resolve` links to the first of them the record draws.\n\n## What a run remembers\n\nThe WORKSPACE remembers what each entity, field, select option, role and\ntemplate alias became here, plus which record each first row landed on and the\nfile each document path was uploaded as. The model file itself holds no live id\n\u2014 it is the portable half, and the same file applies to a demo workspace and to\na customer\'s \u2014 so the join lives where the things it names live, and one model\napplied to two workspaces holds two bindings that know nothing of each other.\n\nIt is the workspace\'s and not the file\'s because a model file is never\ncommitted: memory kept beside it is one author\'s disk, absent for a teammate, on\na second machine or after a delete \u2014 and every one of those goes quietly back to\nbinding by label, which is what grows the second table.\n\nEvery later apply binds through it: it takes the remembered id first and falls\nback to a label only for an alias nothing has bound \u2014 a field you have just\nadded, or a workspace nothing has applied this model to. That is what makes a\nrelabel on EITHER side a rename rather than one thing the workspace lacks and\none the model lacks: `apply` binds the thing it has always meant and reports the\nmove \u2014 which side is right is yours to decide, not a reason to refuse the run.\n\nA bound target the workspace no longer holds IS a refusal, by name: re-binding\nto whatever carries that label today is how the model comes to point at\nsomebody else\'s table. On the CLI, `lotics run restore_table` puts a deleted table back.\n\n`lotics model pull` writes a workspace that already works as a model file \u2014 the\nstarting point for another business\'s model, never a source of truth: it carries\none business\'s words and stops describing that workspace the moment either\nchanges.\n\n## A complete model\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "logo", "label": "Logo", "type": "files" },\n {\n "alias": "tier",\n "label": "Tier",\n "type": "select",\n "options": [\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "default": ["standard"]\n },\n {\n "alias": "orders",\n "label": "Orders",\n "type": "select_record_link",\n "target_entity": "order",\n "cardinality": "many",\n "sync_both_ways": true,\n "paired_field_alias": "customer",\n "display_field_aliases": ["code"]\n },\n {\n "alias": "total_ordered",\n "label": "Total ordered",\n "type": "rollup",\n "source_field_alias": "orders",\n "aggregate_option": { "operation": "sum", "field_key": "amount" }\n }\n ],\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "type": "select",\n "field_key": "tier",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{ "field_key": "name", "order": "asc" }]\n }\n ]\n },\n {\n "alias": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": [\n { "alias": "code", "label": "Order no.", "type": "text", "unique": true },\n { "alias": "placed_on", "label": "Placed on", "type": "date", "format": "date" },\n {\n "alias": "amount",\n "label": "Amount",\n "type": "number",\n "notation": { "style": "currency", "currency": "VND" }\n },\n {\n "alias": "total",\n "label": "Total with VAT",\n "type": "formula",\n "formula": { "expression": "{amount} * 1.1", "notation": { "style": "currency", "currency": "VND" } }\n },\n {\n "alias": "customer",\n "label": "Customer",\n "type": "select_record_link",\n "target_entity": "customer",\n "cardinality": "one",\n "sync_both_ways": true,\n "paired_field_alias": "orders",\n "display_field_aliases": ["name"]\n },\n { "alias": "note", "label": "Note", "type": "text" }\n ]\n }\n ],\n "roles": [{ "alias": "sales", "label": "Sales" }],\n "records": {\n "customer": { "title": "name", "image": "logo", "party": "organization", "figure": "total_ordered" },\n "order": { "title": "customer", "subtitle": ["code", "placed_on"], "figure": "amount", "starts": { "placed_on": "today" } }\n },\n "apps": [\n {\n "alias": "customers",\n "name": "Customers",\n "entity": "customer",\n "register": {\n "columns": ["tier"],\n "readings": [{ "breakdown": "customer", "by": "tier", "value": "total_ordered" }]\n },\n "record": {\n "sections": [\n { "title": "Account", "fields": ["tier"] },\n { "title": "Orders", "blocks": [{ "rows": "order", "columns": ["total"], "create": ["code", "amount"] }] }\n ]\n }\n },\n {\n "alias": "orders",\n "name": "Orders",\n "entity": "order",\n "register": {\n "filters": ["customer"],\n "create": ["code", "customer", "placed_on"],\n "readings": [{ "trend": "order", "over": "placed_on", "value": "amount" }]\n },\n "record": { "door": "drawer", "sections": [{ "title": "Order", "fields": ["note"] }] }\n }\n ],\n "rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } },\n { "ref": "bluebird", "fields": { "name": "Bluebird Foods", "tier": "standard" } }\n ],\n "order": [\n {\n "ref": "so_1001",\n "fields": {\n "code": "SO-1001",\n "placed_on": "@month-start+2",\n "amount": 4200000,\n "customer": "customer:acme"\n }\n },\n {\n "ref": "so_1002",\n "fields": {\n "code": "SO-1002",\n "placed_on": "@today-3",\n "amount": 1150000,\n "customer": "customer:bluebird"\n }\n }\n ]\n }\n}\n```\n\nEach app of this file, as its reader will see it:\n\n```\nApp customers \u2014 "Customers" over Customers (customer)\n Register \u2014 table (default), newest first (default)\n row [Logo] \xB7 Name \xB7 figure Total ordered\n columns Tier\n filters \u2014\n readings "Tier" \u2014 Total ordered summed per Tier, over the rows in view (default)\n add Name (default)\n Record \u2014 a drawer (default)\n header image Logo \xB7 title Name \xB7 figure Total ordered\n checks \u2014\n header \u22EF Delete\n sections 1. "Account"\n fields Tier\n 2. "Orders"\n block Orders (default) \u2014 rows of Orders through Customer; columns Total with VAT; add asks Order no., Amount\n thread \u2014\n history \u2014\n acts \u2014\n Reads Customers, Orders\n\nApp orders \u2014 "Orders" over Orders (order)\n Register \u2014 table (default), newest first (default)\n row Customer \xB7 under it Order no., Placed on \xB7 figure Amount\n columns \u2014\n filters Customer\n readings "Amount" \u2014 Amount summed per period by Placed on, over the rows in view (default)\n add Order no., Customer, Placed on \u2014 starting Placed on at today\n Record \u2014 a drawer\n header title Customer \xB7 subtitle Order no., Placed on \xB7 figure Amount\n checks \u2014\n header \u22EF Delete\n sections 1. "Order"\n fields Note\n thread \u2014\n history \u2014\n acts \u2014\n Reads Orders, Customers\n\n```\n\nEach `(default)` is a value the model left to the system: the door a record opens\nthrough \u2014 a drawer, since neither record has a stage or more than three sections \u2014 the\norder rows open in, and what an add asks. The order\'s one section holds every field a\nperson writes that the header does not already show. The customer\'s orders\nare a block in its "Orders" section, never its own `Orders` link as a field as well:\none fact, one place. An order is titled by its customer and read by its number under\nit; the block names what its add asks, since a subtitle or a figure is never asked by\ndefault. `Total ordered` is a rollup, so it is read and never asked; the\norders app\'s record opens in a drawer, because its rows are worked one after another.\n';
|
|
44534
|
+
var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOne JSON file describing a workspace: its tables, fields, options, views, roles,\nfirst rows, how a row of each table is recognised, and the apps over them. Each \xA7\nis its own page at `model/<section>` \u2014 `lotics docs`, or the `docs` tool. Which treatment each job\'s\nscreen needs: the `design` reference. Each treatment worked through in an app of a complete model: the\n`examples` reference.\n\n**What a model composes with**\n\n- **Entities and fields** (\xA7 Entity, \xA7 Field) \u2014 the tables, their columns, options and links.\n- **Records** (\xA7 Records, then \xA7 Status) \u2014 how a row of each entity is RECOGNISED: its title, the line\n under it, its picture, its status, the one number it stands for. Stated once per entity\n and read by every surface that draws one of its rows.\n- **Write rules** (\xA7 Write rules) \u2014 what a write finds, bounds and picks among.\n- **Apps** (\xA7 Apps, then a page each: \xA7 Register, \xA7 Layouts, \xA7 Tabs, \xA7 Export, \xA7 Readings,\n \xA7 Averages and bands, \xA7 Dashboards, \xA7 Record page, \xA7 Blocks, \xA7 Composed page, \xA7 Publish desk, \xA7 Timeline and agenda, \xA7 Statements, \xA7 Reading blocks, \xA7 Tasks, \xA7 Acts,\n \xA7 Papers and recordings, \xA7 Checks) \u2014 one\n register over one entity and the record each row opens: which fields go where, the acts,\n and the checks that guard them \u2014 or a dashboard of readings.\n How each thing LOOKS is the runtime\'s, one treatment per concept; no key here changes it.\n A screen the job needs and no key states is a `lotics report` \u2014 the job, what the model\n drew, the word wanted \u2014 never keys bent to approximate it.\n\n**The working order**\n\n1. Name the people and each one\'s JOB \u2014 the work they alone decide or write.\n2. The entities and fields those jobs touch, and a `records` entry for every entity an\n app lists, opens or picks.\n3. One app per job in `apps[]` \u2014 a desk a person works daily from its own list \u2014 each one register over one entity; a list worked only in passing is a section of the app it serves.\n4. `lotics model apply model.json` \u2014 the file is checked first, every problem in one run;\n then the tables, then a new version of every app, live\n (`lotics setup model.json --email you@company.com` where no account exists yet).\n5. Change the file and apply it again; `lotics model pull -o model.json` writes what the\n workspace holds, and each apply of that file sends only what it changes. `apply_model` takes\n a `patch` in place of `model`: a JSON merge patch (RFC 7396) over the workspace\'s model, each\n list whose items carry an `alias` written as an object keyed by it \u2014\n `{ "apps": { "desk": { "acts": { "close": null } } } }` removes one act. Where an aliased item\n stands is a move beside the patch, `"moves": [{ "at": "apps.desk.acts.close", "after": "hold" }]`\n (or `"first": true`), placed once the patch merged \u2014 built into an app\'s acts, and a select\'s\n options (`entities.<entity>.fields.<field>.options.<option>`) reordered on its table, the plan\n saying each order it writes; a table\'s\n fields and views and the apps themselves keep the workspace\'s order. A patch whose every key and\n move lies inside one app\'s body leaves the tables as they stand and builds that app, beside any\n other app whose build reads what it changed. A `null` removes only\n what an apply writes as the model states it \u2014 anything inside an app, a `records` entry\n (`"records": { "<entity>": null }`), a write rule, an entity\'s `singular` and `writes`, a\n field\'s `default`, `required` and `writes`, a text field\'s `format`, a number\'s `notation` \u2014 and is refused\n anywhere else, a table, field, option, view, role, template or app among them; one naming\n nothing the model holds changes nothing. `get_model` with `app` reads one app and the\n entities it reads.\n Rolling an app back (`lotics run rollback_app`) restores its earlier version \u2014 table\n changes and data writes stay.\n\n## The rules\n\n- **At least one entity, at most 50.** More tables than that is a data model\n being designed, not applied \u2014 apply the rest in a second call.\n- **An existing table is adopted.** `lotics model apply` binds an entity whose `label`\n already names a table in the workspace, and adds the fields, options and\n views it is missing. No stored value is ever changed or deleted: a field it\n adopts takes the model\'s `default` and `required`, a text field\'s `format`, and\n the `notation` it reads in, where that only relabels \u2014 a date\'s format and a unit\n converting its figures are left. A field the model once stated and no longer does\n stays, with its rows, and is no longer required. A computed field\'s definition follows the model: a formula\'s expression,\n notation and options, a rollup\'s link, aggregation and filter, a\n lookup\'s link, field and order, and an autonumber\'s `template` (new rows only \u2014\n existing numbers keep theirs). A check and the apply name each one that changes.\n Applying the same model twice changes nothing the second time.\n- **Renaming a field is `lotics run update_table`**, then the same label in this\n file; a table is deleted on the CLI by `lotics run delete_table`, or by a member\n in Lotics. Neither goes through the file (\xA7 What a run remembers).\n- **The file\'s own majority is the language.** A model names no locale \u2014 which\n language it is in is what it mostly says, and `model apply` notes the label\n written the other way. The generated screens read the kit\'s pack, and a\n generated WRITE cannot: its refusals run on the server, so they are worded in\n that same majority. Mix the two and the workspace answers in two languages.\n- **Rows land only where every bound table is empty.** One table already holding\n records and no rows are written anywhere: sample rows landing among a\n customer\'s real ones cannot be told apart from them.\n- **`lotics model apply` checks all of it before anything is written**, and\n reports every problem in one run rather than the first. Each section of this\n page ends its keys with the rules the check enforces there, one sentence each\n under an id; a finding names its rule\'s id (`[register.filter]`), and\n `model/<rule id>` is that one rule\'s page.\n\n<!-- generated:start rules-model -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `model.schema` | Every key is one its table lists, holding the type its row gives, and every required key is stated. |\n| `model.retired` | `field_roles`, `table_workflows` and `connections` are no longer part of a model: `records` states how a row is recognised, and an app\'s own writes do what a table automation did. |\n| `model.names-declared` | Every alias a key names is declared: an entity of this model, a field of the entity the key reads, an option of the select it names, an act of the app, a template or an app of this model. |\n| `model.tables` | A model declares at least one table and at most 50; apply the rest as a second model. |\n| `model.rows-cap` | Rows are a sample: at most 200 per entity, 2000 per model and 2000 documents attached \u2014 a real data set belongs in an import. |\n| `model.language` | *Noted, never refused:* A label or description written in the other language than the model\'s majority \u2014 with or without diacritics. |\n| `model.alias-unique` | An alias is unique where it is named: entities, roles and templates in the model, fields and views in their entity, options in their select. |\n| `model.label-unique` | A label is unique where apply finds it by label: entities, roles and templates in the model, fields and views in their entity, options in their select \u2014 and no view takes its entity\'s own label, which names the whole-table grid apply makes. |\n| `model.template-sha` | A template\'s `content_sha256`, where stated, is the 64-character lowercase hex sha256 of its content. |\n| `model.template-kind` | An act\'s paper and a register\'s `export` are made from an html or an excel template, never an email one. |\n| `model.template-act` | *Refused until adopted or declined:* In a model stating apps none of whose acts runs an authored `workflow`, every html or excel template is made by an act (`template`, `templates`) or filled by a register\'s `export` \u2014 the patch adds the act to the app over the entity whose fields it prints most. |\n| `model.filter` | A filter \u2014 a view\'s, a rollup\'s \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares. |\n\n<!-- generated:end rules-model -->\n\n## Top level\n\nEvery table of keys on this page is generated from the schema `model apply`\nparses the file with, so it is the whole of what a key may hold.\n\n<!-- generated:start top-level -->\n\n#### Model file\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entities` | list of [Entity](#entity) | yes | The tables this model creates, with their fields, options and views |\n| `roles` | list of [Role](#role) | no | Workspace groups to create; members are added to them afterwards |\n| `templates` | list of [Template](#template) | no | Document templates: html and email inline, excel made from an uploaded workbook |\n| `rows` | map of alias \u2192 list of [Row](#row) | no | First records, keyed by entity alias \u2014 written only where every table they land in is empty |\n| `records` | map of alias \u2192 [Record](#record) \\| `null` | no | How a row of each entity is recognised, keyed by entity alias. Every entity an app lists, opens or picks has one; `null` removes the entry its table holds |\n| `write_rules` | map of alias \u2192 [Entity write rules](#entity-write-rules) | no | Entity alias \u2192 what a write of that entity finds, bounds and picks among |\n| `apps` | list of ([App](#app) \\| [Dashboard app](#dashboard-app)) | no | The apps this workspace will have \u2014 each one register over an entity, or a dashboard of readings |\n\n<!-- generated:end top-level -->\n\n**A model carries no** `fixtures`, `knowledge` or `knowledge_expects`, and no\n`word` / `pdf-form` template: file content is uploaded to the workspace, never\nstated in a model \u2014 an `excel` template names its uploaded workbook by `file_id`. `apps` here is what an agent states to make an app,\nnever built code. An unknown top-level key is an error, never ignored.\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how the file cross-references\nitself; they are never shown to anyone. `label` is what a person sees.\n\nLabels must be unique within their namespace \u2014 two entities, two fields on one\nentity, two options on one field, two views on one entity, two roles or two\ntemplates cannot share a label, because `apply` matches by label.\n\n## Entity\n\n<!-- generated:start entity -->\n\n#### Entity\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable entity alias, unique within the contract |\n| `label` | text | yes | The table\'s name in the workspace, which apply names it |\n| `singular` | text | no | One row of this table, in the business\'s own words \u2014 what a create\'s button and panel name |\n| `description` | text | no | The table\'s description, written onto the table in the workspace |\n| `writes` | `false` | no | false: no app opens or edits this table\'s rows. Direct writes follow the table\'s access, as on any table |\n| `fields` | list of [Field](#field) (at least one) | yes | The table\'s columns |\n| `read_scope` | [Read scope](#read-scope) | no | Which rows a member reads. Absent, every member with access to the table reads every row. |\n| `unique` | list of list of alias (at least one) (at least one) | no | Sets of fields whose values no two live rows share \u2014 each a list of field aliases (text, number, date, a single select, or a link of cardinality "one"). A create or update landing a second row with the same values is refused. |\n| `views` | list of [View](#view) | no | Saved views, in the order they are listed; with none, the table still opens on its default grid |\n\n#### Read scope\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `any` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option)) (at least one) | yes | A row is readable when ANY of these holds |\n\n#### Read scope by role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `member_of` | alias | yes | A role alias: whoever is in the group it binds to reads the row |\n\n#### Read scope by member\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A select_member field alias on this entity, or on the entity `through` lands on |\n| `is` | `"self"` | yes | The members this column names on a row read that row |\n\n#### Read scope by option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A single-select field alias on this entity, or on the entity `through` lands on |\n| `is` | list of alias (at least one) | yes | Its option aliases whose rows are readable \u2014 naming none would hide every row |\n\n<!-- generated:end entity -->\n\n<!-- generated:start rules-entity -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `entity.singular` | An entity a create is mounted over \u2014 a register that adds, a rows block \u2014 states its `singular`: one row of it, in the business\'s words. |\n| `entity.required-cycle` | Required links never wait on each other in a loop: no row of the loop could be created first. |\n| `entity.link-format` | A text field of `format: "link"` holds web addresses; one whose rows hold a phone or a mail address states no format. |\n| `entity.read-scope` | A `read_scope` clause names a declared role (`member_of`) or a field of its entity reached through one-row links: `"self"` a member field, options a single select declares. |\n| `entity.unique` | A `unique` set names two fields or more of its entity once each, each holding one value to compare \u2014 text, a number, a date, a single select, a one-row link \u2014 and is stated once; one field alone is `unique: true` on it. |\n\n<!-- generated:end rules-entity -->\n\n**`unique` is a set of values no two live rows share.** Each entry names fields\nof this entity holding ONE value \u2014 text, number, date, a single select, a link\nof cardinality `"one"` \u2014 and a create or update landing a second row with the\nsame values is refused. A set of one text field is that field\'s own `unique:\ntrue`, so it is refused here. A create carries each set in the names its panel\nsends, so the panel can name the duplicate before the write does.\n\n**`singular` is what a create says** \u2014 `New Order`, `Add Claim line`, `H\u1ED3 s\u01A1\nm\u1EDBi` \u2014 while `label` names the table, so without it the button reads `New\nOrders`. Nothing derives it: English plurals are irregular, and no language is\nexempt. In a language without plural forms it is usually the label itself,\nless any word for the collection. `model apply` REFUSES a model where a table\nsome create opens \u2014 a register\'s own, or a record section\'s add \u2014 states none.\n\n**`writes: false` says no app opens, edits or files these rows** \u2014 a log of what\nthe system sent, a copy of what another system holds. A record listing them\nkeeps the section and opens each row at rest, with no Add; a screen over the\nentity operates at most the rows its record owns; a party of it is picked, never\nfound or minted; and the generator writes no create and no update for it, and\nnever notes it as created nowhere. It shapes the apps, not who may write: a\ndirect write \u2014 the records API, the chat agent, an import \u2014 follows the table\'s\naccess, as on any table. A `lifecycle` on it is refused \u2014 a row walked\nthrough stages is worked by a person \u2014 and so is a publish desk over it (a rows\nblock\'s `post`), whose ticks make its rows. Absent,\napps open and edit the rows; `true` is not a value.\n\n**`read_scope` is a ROW rule, enforced by the platform.** It is resolved at\napply into the table\'s own row filters, so an app, a workflow reading for a\nviewer, and the API all answer the same rows \u2014 a per-record visibility field the\napp merely honours is a convention, not a gate. A `"self"` clause reads a column\nof one member or several, and every role alias and option alias a clause names\nmust be one this model declares. `apply` writes the rule onto a table it\nCREATES; a table it adopted that ALREADY CARRIES a rule keeps that one, because\nthe rule is the workspace\'s own statement about its rows \u2014 and a run whose model\nstates a different rule reports the entity rather than leaving the claim silent.\n\n**An app may state that its sharing is its read gate: `"reads": "shared"`.** An\napp reads as its owner, so the rule reaches a viewer only as the predicate every\nquery and editor guard of every app over the entity carries \u2014 right for a desk of\none\'s own rows, wrong for a desk whose audience its sharing already decides, where\nwidening the rule meant a role group nobody remembers to fill. Stated on an APP,\nits queries, pickers and guards carry no entity\'s rule and whoever the app is\nshared with reads and writes every row it draws; every other app and the table\'s\nown filters keep the rule. Share it deliberately. Refused on an app none of whose\ntables states a `read_scope`.\n\n**A row rule is never inherited.** A child read under a private record states\nits own: the record\'s `member_of` clauses as they are, and its column clauses\n`through` the one-row link the child hangs by \u2014 over one hop or several, each a\nlink of `"cardinality": "one"`, since over several rows the rule would admit a\nreader any one of them admits while the editor\'s guard reads the first. Only the\nstatus history takes its record\'s rule.\n\n## Field\n\nEvery field carries the keys below, and its `type`\'s section adds the rest; a\ntype whose section names no `default` takes none. `label` may not contain `{` or\n`}` (formulas reference fields by label at the platform level). Every row in `rows` states each\n`required` field it carries (neither a default nor a `default_from` copy is applied to them), and a required\nLINK is written with its row: the entity it names is created first, and entities\nwhose required links name each other are refused, since none of their rows could\never be created.\n\n**A stored field\'s `writes: false` says no person keys it in an app** \u2014 a post\'s\nid a publishing workflow brings back, a count a sync fills. Every app draws it\nread wherever it is placed, no add or act asks it, no paper fills it, and the\ngenerator writes it in no create and no update; a statement line reads it as a\nfigure no person keys, and beside `default_from` it is copied when the row is\nmade and never keyed. It shapes the apps, not who may write: a workflow, the\nrecords API, the chat agent and an import write it as the table\'s access allows.\nA formula, a rollup, a lookup, an autonumber and a date `derive_from` stamps take\nno such key \u2014 no one keys them already. Absent, a person keys the field; `true`\nis not a value.\n\n<!-- generated:start field -->\n\n#### Field\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"date"` \\| `"boolean"` \\| `"select"` \\| `"select_member"` \\| `"select_record_link"` \\| `"files"` \\| `"formula"` \\| `"rollup"` \\| `"lookup"` \\| `"autonumber"` | yes | What the field holds \u2014 each type takes the further keys its own section lists |\n| `alias` | alias | yes | Stable local alias, unique within the entity |\n| `label` | text | yes | The field\'s name in the workspace, which apply names it |\n| `description` | text | no | The field\'s description, written onto the field in the workspace |\n| `required` | boolean | no | Refuse a record whose cell for this field is empty. Apply writes it onto the field, and every write path \u2014 create, update, an agent\'s tool call, a workflow\'s set \u2014 refuses the row by field name. |\n\n<!-- generated:end field -->\n\n### `text`\n\n<!-- generated:start field-text -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `unique` | boolean | no | Unique values required |\n| `format` | `"text"` \\| `"link"` \\| `"markdown"` | no | How the words are drawn \u2014 plain, as a link that opens, or as markdown |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-text -->\n\n### `number`\n\n<!-- generated:start field-number -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | number | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. Omitted, a decimal. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-number -->\n\n`notation` is how the number reads, and every surface reads it:\n`{ "style": "currency", "currency": "USD" }` prints as money in its code,\n`{ "style": "unit", "unit": "kg" }` as a quantity with its unit after it,\n`{ "style": "unit", "unit": "percent" }` as a percent \u2014 10 is "10%", never\nmultiplied \u2014 and `{ "style": "decimal" }`, or no notation, as a plain number. One\nkey, so a figure reads one way.\n\nThe keys `notation` replaced \u2014 `format`, `currency`, `unit`, `unit_field` and\n`currency_field`, on a number or in a formula, and a formula\'s `format: "link"`\nfor `link: true` \u2014 are no longer read. A field stating them naming another\nreading than its `notation` (a decimal, where it states none) is refused, and the\nfinding names the `notation` to write in their place: `"format": "currency",\n"currency": "USD"` becomes `"notation": { "style": "currency", "currency": "USD" }`.\nWrite `notation`.\n\nAn ABSENT number is drawn absent \u2014 the one exception is a\n`sum` or count rollup the plan reads as a **`measure`**: that is the thing\naccumulated toward a bound, so nothing accumulated yet is zero and the meter\ndraws it. The same rollup read as an `amount` keeps its blank, and so does every\nother role: nothing added to what a row is WORTH means unpriced, not free. A\nformula reading only such sums and counts, and reading zero where each does\n(`{received} - {refunded}`), is one too. A `min`, an `avg`, a percentage of\nnothing and every other formula stay blank in any role, because none of them has\nan answer to give. This is why the pair on one\nscreen reads two ways \u2014 what has come in against what is owed \u2014 and why a\nmeasure\'s own LIMIT, an amount, leaves an unquoted row out of the count rather\nthan reporting it as nothing collected. **AND WHERE THAT LIMIT IS ABSENT \u2014 OR\nZERO \u2014 THERE IS NO LEVEL AT ALL**: a level is a reading AGAINST a bound, so a row\nthat states no bound, or a bound of nothing, draws nothing \u2014 cell, fact and all \u2014\nrather than a numerator whose whole meaning was the comparison. "Collected 0"\nbeside a blank total reads as money against a job worth nothing, and "0 of 0"\nagainst a count of nothing owed claims a comparison nobody can make. A measure the model gives no limit is a plain figure\nand is unaffected. A share is stored in percent units \u2014 68.1 is 68.1 % \u2014 and the\ncolumn, the fact behind it, the meter it is judged by and the figure over the\nregister all say so.\n\nA unit \u2014 `{ "style": "unit", "unit": "kg" }` \u2014 is what a figure counts or\nmeasures; `percent` aside, it is measured or counted. A **measured** unit is a code of one catalog \u2014 mass `g`\n`kg` `t`, volume `l` `m3` `cbm` (a cubic metre under freight\'s name), length\n`mm` `cm` `m` `km`, area `m2`, duration `min` `h` `day` \u2014 so a figure typed in\nanother unit of its dimension converts (`12,5 t` into a `kg` field is 12 500),\nand a tile or a chart reads it in the largest unit it reaches (12 500 kg as\n12,5 t\u1EA5n; CBM never scales) while a cell, a fact and a column always read in the\nfield\'s own. Any other noun of at most 12 characters (`ki\u1EC7n`, `pallet`, `TEU`) is a\n**counted** unit: a word after the figure that never converts. A measured unit is\nwritten as its code \u2014 `t\u1EA5n`, `KG` or `m\xB3` is refused, naming the code. A formula\nstates its own in `formula.notation`; a rollup that keeps the value (`sum`, `avg`,\n`median`, `min`, `max`, `range`) and a lookup carry the unit of the figure they\nread, exactly as they carry a currency, and a count carries none. A quantity is a\nnumber with its unit, never words (`3 cartons` in a text field): only a number\nsums, converts and reads down a column.\n\nA field\'s cells always hold figures in its own unit, so moving it between two\nunits of one dimension (`kg` to `t`) converts every stored figure \u2014 a change made\nin the workspace, in the field\'s settings (which say how many first) or through\n`lotics run update_table`. `model apply` never makes it: the model states the\nworkspace\'s unit until then. Any other change of unit\nrelabels.\n\nA figure whose unit or currency varies by row names a single select of its own\nrow as `per_row` \u2014 `{ "style": "unit", "unit": { "per_row": "uom" } }`,\n`{ "style": "currency", "currency": { "per_row": "currency" } }` \u2014 or a lookup of\none through a one-link, a line reading its shipment\'s currency. That select\'s\noptions are the vocabulary: each label a unit (`chi\u1EBFc`, `kg`), or an ISO 4217 code\n(`USD`). A label outside it is refused when the figure is written and when the\nselect is \u2014 its options, its type or its deletion while a figure names it. A row\nwhose select is empty reads its figure bare. Every app reads each row\'s figure in\nits own row\'s unit, and a sum never mixes units: totals are one per unit, a\nchart draws one at a time. A rollup that keeps the value (`sum`, `avg`, `min`\u2026)\nover such a figure stands only where the child\'s select is a lookup, through\nthe rollup\'s own link, of a select on this entity \u2014 the rollup reads that\nselect\'s unit; otherwise it is refused, as is any lookup of such a figure (its\nunit lives on the other row). A count is unaffected. Moving a field between a\nfixed and a per-row unit relabels.\n\n### `date`\n\n<!-- generated:start field-date -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. A date string in the field\'s format. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `format` | `"date"` \\| `"datetime"` \\| `"date_range"` \\| `"datetime_range"` | no | Whether the field holds a day or a moment, alone or as a span |\n| `timezone` | text | no | IANA timezone |\n| `derive_from` | `"created_at"` \\| `"updated_at"` | no | Auto-populate from the row\'s system timestamp; the field becomes read-only. |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-date -->\n\nA `default` is refused beside `derive_from`: the platform stamps that date.\n\n### `boolean`\n\n<!-- generated:start field-boolean -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | boolean | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-boolean -->\n\n### `select`\n\n<!-- generated:start field-select -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | list of alias | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. Option alias(es) this field declares \u2014 one for single-select. |\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `options` | list of [Select option](#select-option) (at least one) | yes | The choices, in the order every picker and every ladder lists them |\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n#### Select option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable local alias, unique within the field |\n| `label` | text | yes | Display label for the option |\n| `color` | [colour](#select) | yes | The colour the option\'s badge is drawn in |\n| `mark` | [mark](#select) | no | The option\'s own mark, drawn in place of its colour dot wherever the option is shown: the brand it is ({kind: "brand", name: one of facebook, instagram, threads, meta, tiktok, google-ads, zalo, linkedin, x, google-meet, youtube, telegram, whatsapp, gmail, google-drive, outlook, kiotviet, misa, lark, payos}) a kit glyph ({kind: "icon", name: "wrench"}) or the face of who it is, drawn from its label ({kind: "avatar", name: "person" or "organization"}). Every option of a field has one, or none does. A mark a reader does not draw falls back to the dot |\n\n<!-- generated:end field-select -->\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\nEvery option is drawn in its colour wherever it shows \u2014 a cell, a row\'s line,\na record, a picker, the bar a reading splits the rows by.\n\nAn option may carry its own `mark`, drawn in place of its colour dot wherever\nthe option is shown \u2014 a stage, a chip, a filter, a fact, an entry of a log, a\nreading\'s part, a lookup of the select on another entity. Where the select is its\nrows\' `records` `mark`, a brand stands on the row\'s lead tile and any other option\nleads with its colour dot there. A select a reader scans\ndown a column \u2014 how a payment was made, the channel, the mode \u2014 states one on every\noption, since a glyph reads before its word: the brand it IS\n(`{"kind": "brand", "name": "tiktok"}` \u2014 one of `facebook`, `instagram`,\n`threads`, `meta`, `tiktok`, `google-ads`, `zalo`, `linkedin`, `x`,\n`google-meet`, `youtube`, `telegram`, `whatsapp`, `gmail`, `google-drive`,\n`outlook`, `kiotviet`, `misa`, `lark`, `payos`), a glyph the kit draws\n(`{"kind": "icon", "name": "wrench"}`; any other name is refused), or \u2014 where the\noptions name people or organisations who are no member and no row \u2014 the face of who\nit is, drawn from its label (`{"kind": "avatar", "name": "person"}` or\n`"organization"`). Every option of a select has one, or none does: a run of chips\nwhere one carries no mark reads as the one missing something. `apply` writes the\nmarks onto the table, sets one an adopted option lacks, and reports one it wears\ndifferently rather than overwrite it. The glyphs:\n\n<!-- generated:start field-select-glyphs -->\n\nactivity, align-center, align-left, align-right, align-vertical-justify-center, align-vertical-justify-end, align-vertical-justify-start, arrow-down, arrow-down-a-z, arrow-down-up, arrow-down-wide-narrow, arrow-left, arrow-left-from-line, arrow-right, arrow-right-from-line, arrow-right-left, arrow-up, arrow-up-down, arrow-up-wide-narrow, arrow-up-z-a, ban, banknote, bed, bell, between-horizontal-start, between-vertical-start, bold, bolt, book-marked, book-open, book-text, bot, box, brackets, brain, briefcase, building-2, calculator, calendar, calendar-clock, calendar-off, camera, car, chart-column, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down-up, chevrons-up-down, circle-alert, circle-check, circle-x, clipboard-list, clipboard-paste, clock, code, code-xml, columns-3, columns-3-cog, combine, construction, container, copy, credit-card, database, decimals-arrow-left, decimals-arrow-right, dollar-sign, download, ellipsis, eraser, expand, external-link, eye, eye-off, facebook, file, file-csv, file-down, file-question, file-spreadsheet, file-stack, file-text, file-up, folder, folder-closed, folder-open, folder-pen, form, funnel-plus, funnel-x, gauge, globe, gpu, grid-2x2, grip-vertical, group, hand-coins, hash, heading, heading-1, heading-2, heading-3, history, house, image, inbox, info, instagram, italic, keyboard, languages, layout-dashboard, layout-grid, library-big, link-2, link-2-off, linkedin, list, list-checks, list-collapse, list-filter, list-filter-plus, list-ordered, loader, lock, lock-keyhole, lock-keyhole-open, lock-open, log-in, log-out, mail, map-pin, maximize-2, megaphone, menu, message-circle, message-circle-question-mark, message-square, messages-square, mic, minimize-2, minus, monitor, mouse, mouse-pointer-click, music, newspaper, notepad-text-dashed, package, paint-bucket, paintbrush-vertical, palette, panel-left, panel-left-close, panel-left-open, panel-right, panel-right-close, panel-right-open, paperclip, pause, pencil, percent, phone, pin, pin-off, plane, play, plug, plus, receipt, rectangle-ellipsis, redo, refresh-cw, repeat, rotate-ccw, rotate-cw, rows-3, scan, scan-line, scissors, search, send, settings, share, share-2, shield, shield-alert, shield-check, shopping-cart, sigma, sliders-horizontal, smile, smile-plus, snowflake, sparkles, split, square, square-check, square-pen, square-sigma, stethoscope, sticky-note, strikethrough, table, table-2, tag, target, text-quote, text-wrap, thumbs-down, thumbs-up, ticket, trash, trending-down, trending-up, triangle-alert, truck, tv-minimal, twitter, type, underline, undo, upload, user, user-check, user-pen, users, utensils, waypoints, workflow, wrench, x, zap, zoom-in, zoom-out\n\n<!-- generated:end field-select-glyphs -->\n\n```jsonc\n"options": [\n { "alias": "short_video", "label": "Short video", "color": "zinc", "mark": { "kind": "brand", "name": "tiktok" } },\n { "alias": "print", "label": "Print", "color": "amber", "mark": { "kind": "icon", "name": "newspaper" } }\n]\n```\n\n### `select_member`\n\nA person picker over the workspace\'s members. No default: a model cannot name\nmembers of a workspace that does not exist yet. With no role it is still drawn \u2014\nface and name, ranked as a `party` \u2014 in a screen\'s `columns` and in the register\na record draws of these rows.\n\n<!-- generated:start field-select_member -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `multi` | boolean | no | Allow multiple selections |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-select_member -->\n\n### `select_record_link`\n\n<!-- generated:start field-select_record_link -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n| `target_entity` | alias | yes | Alias of the entity this field links to |\n| `sync_both_ways` | boolean | no | Create a paired link field on the target entity for bidirectional sync |\n| `paired_field_alias` | alias | no | The pair edge of a bidirectional link: the field alias ON THE TARGET ENTITY that is this link\'s sync partner. Both sides of a pair carry it, each naming the other. Apply creates whichever side it reaches first WITH the pairing (the platform auto-creates the partner) and binds the partner alias to the auto-created field \u2014 without this edge the two contract fields would be created independently and collide with the auto-created partner. |\n| `cardinality` | `"one"` \\| `"many"` | no | How many linked records this field holds. Default \'many\'. \'one\' holds a single row and needs no partner; where the link IS paired, the partner side holds many. |\n| `display_field_aliases` | list of alias | no | Field aliases on the target entity shown as the link\'s display text / picker columns |\n| `default_from` | text | no | Copied when a record is made off the row a one-row link of this entity names \u2014 `<link alias>.<field alias>` \u2014 and never re-read, so the source changing later leaves the row alone. A value the add states wins; a select copies each option as this field\'s option of the same alias. |\n\n<!-- generated:end field-select_record_link -->\n\nA two-way link is declared on BOTH sides, each naming the other as its\n`paired_field_alias`; the pair must be symmetric or the model is refused.\n\n**A single-valued link needs no partner.** `"cardinality": "one"` on its own is a\nlink that holds one row \u2014 one customer on an invoice, one project on a device \u2014\nand nothing is created on the target. The mirror invariant belongs to a PAIRED\nlink: pair a link when the target\'s own record should list what points at it, and\nleave it unpaired when it should not. Either way the record plan draws the\nrelation as a section on the side it points at, so an unpaired link costs the\ntarget nothing.\n\n### `files`\n\nA row attaches documents to it (\xA7 Rows). Each file\nis drawn by its kind, with no key to choose: a picture (an image, a video) as its\nthumbnail, a document (a PDF, a sheet) as its type\'s badge and its filename.\n\n<!-- generated:start field-files -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `writes` | `false` | no | false: no person keys this field in an app \u2014 a workflow or an integration writes it. Every app draws it read, no add or act asks it and no paper fills it. Direct writes follow the table\'s access, as on any field |\n\n<!-- generated:end field-files -->\n\n### `formula`\n\n<!-- generated:start field-formula -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | [Formula](#formula) | yes | Formula config. The expression references other fields on the SAME entity by alias in braces, e.g. `{quantity} * {unit_price}`. |\n\n#### Formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `expression` | text | yes | The expression, over fields of THIS entity by alias in braces \u2014 `{quantity} * {unit_price}` |\n| `notation` | [notation](#number) | no | How the figure reads: { "style": "decimal" }; money, { "style": "currency", "currency": an ISO 4217 code (USD, VND) }; or a quantity, { "style": "unit", "unit": "percent" (10 reads "10%"), or a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does }. A currency or unit read on each row from a single select of that row is { "per_row": that select }. For a formula yielding a number. |\n| `link` | `true` | no | The formula yields a URL as text, drawn as a link that opens. |\n| `options` | list of [Select option](#select-option) (at least one) | no | The categories the formula yields, drawn as a single select\'s options are (read-only). The expression yields one of them as `{this_field:option}`, or null \u2014 `{days_idle} > 30 ? {warmth:cold} : {warmth:hot}`. Omit for a formula yielding a plain value. |\n| `output_type` | `"number"` \\| `"text"` \\| `"date"` \\| `"datetime"` \\| `"boolean"` \\| `"select"` | no | What the expression YIELDS \u2014 the kind the platform infers at write time, declared here so the offline checks can read it. `notation` beside it is how a number result reads, not what it is. Ignored on the wire (the platform re-infers it); `lotics model pull` writes the inferred value. |\n\n<!-- generated:end field-formula -->\n\n`output_type` is what lets a role or a screen clause accept a computed value: a\ncaption over a derived name (`output_type: "text"`), a period over a settled date\n(`"date"`). A formula declaring no `output_type`, `notation` or `link` says nothing\nabout its result, and every rule that needs one refuses it by name. A formula stating\n`options` is a computed category: read-only, and read as a single select\nwherever it is drawn \u2014 a column, a filter, a reading\'s `by` or `where`.\n\n**A select reaches a formula as the KEYS of its chosen options**, a list \u2014\nnever their labels, and never their aliases \u2014 and a model has no keys: the\nworkspace mints them when the table is made. So an option is named in a formula\nas `{field:option}`, both aliases, and the copy writes that option\'s key in its\nplace: `includes({kind}, {kind:crate})` for a select holding one or several,\n`{kind}[0] == {kind:crate}` for a single one. A select compared to its own words\n(`{kind} != "Crate"`) matches no row and computes the other branch everywhere,\nso the check refuses it and names the token. A select looked up from another\nentity is tested there, in a formula of its own, and that result looked up.\n\n**The language.** The offline check and the platform compute a formula with one\nengine, and the check refuses a call or a name it cannot run, naming what to write:\n\n<!-- generated:start field-formula-language -->\n\n- Operators: `+ - * / %`, `== != > < >= <=`, `&& || !`; `+` also joins text\n- Conditionals: a ternary only \u2014 `{amount} > 100 ? "High" : "Low"`\n- Not supported: optional chaining (`?.`), nullish coalescing (`??`), template literals, arrow functions \u2014 use `get(obj, "path", default)`, `coalesce(v1, v2)`\n- Helpers are these names, spelled exactly; a spreadsheet function (`IF`, `SUM`, `LEN`, `DATEDIF`) is none of them, and a formula calling one is refused\n- Math: round(n,decimals?), ceil(n), floor(n), abs(n), min(a,b), max(a,b), sum(arr), mean(arr), clamp(n,min,max), percentage(part,total,decimals?), pow(base,exp), sqrt(n), mod(n,divisor)\n- Strings: upper(s), lower(s), trim(s), capitalize(s), length(s), contains(s,search), join(arr,sep), split(s,sep), replace(s,search,rep), replaceAll(s,search,rep), startsWith(s,prefix), endsWith(s,suffix), substring(s,start,end?), padStart(s,len,char), padEnd(s,len,char), numberToWords(n, lang?) (lang \'vi\' default, or \'en\')\n- Lists: includes(list,value), first(list), last(list), unique(list), compact(list) \u2014 length(list) counts one\n- Dates: now(), formatDate(d,fmt), addDays(d,n), subDays(d,n), addHours(d,n), subHours(d,n), addMinutes(d,n), subMinutes(d,n), startOfDay(d), endOfDay(d), differenceInCalendarDays(later,earlier), differenceInHours(later,earlier), differenceInMinutes(later,earlier), isBefore(d1,d2), isAfter(d1,d2), isSameDay(d1,d2), isToday(d), isWithinRange(d,start,end), parseDate(d)\n- Null/type: isNull(v), isEmpty(v) (also true for "" and []), coalesce(v1,v2,...), isString(v), isNumber(v), isBoolean(v), isArray(v), toNumber(v), toString(v)\n- Other: formatCurrency(amount,locale,currency), formatDecimal(value,decimals,locale) (grouped quantity, no symbol), get(obj,"path",default?)\n- Empty cells: a cell nobody filled is null inside a formula, whatever its type; one holding 0, false or "0" is not empty. Test it with `isEmpty({note})` \u2014 `{note} == ""` and `{done} == false` are false on an unset cell\n- Arithmetic over an empty cell: `+` and `-` read it as 0 beside a value (`{fee} + {surcharge}` is `{fee}` when the surcharge is empty, null when both are); `*`, `/`, `%` and a unary `-` yield null (`{price} * {qty}` is null, not 0, when the quantity is empty)\n- No helper throws on an empty cell: the math helpers and toNumber return null, the string helpers "". A value of the wrong type still errors. When EVERY field a formula reads is empty it is null \u2014 unless it reads each only as the argument of isEmpty, isNull or isNotNull (`!isEmpty({file})` is false there, not null)\n\n<!-- generated:end field-formula-language -->\n\n### `rollup`\n\nAggregates the records reached through a link on this entity.\n\n<!-- generated:start field-rollup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to roll up from |\n| `aggregate_option` | [aggregation](#rollup) | yes | Aggregation operation. Its `field_key` names a field alias on the linked entity. |\n| `filter` | [filter](#views) group | no | Only linked records matching this filter are aggregated; one condition on its own is a group of one. Every `field_key` in it names a field alias on the linked entity, and a select condition\'s value names an option alias there. A traversal node reaches past that entity, so its `path` hops and inner `field_key` are fully-qualified `entity.field` aliases. |\n\n<!-- generated:end field-rollup -->\n\n`aggregate_option` is `{ "operation": \u2026, "field_key": \u2026 }` \u2014 `field_key` a field\nalias on the linked entity (`count` may omit it), and `operation` one of `count`,\n`sum`, `avg`, `median`, `min`, `max`, `range`, `empty`, `filled`,\n`percent_empty`, `percent_filled`, `unique`, `percent_unique`, `earliest`,\n`latest`, `date_range`, `checked`, `unchecked`, `percent_checked`,\n`percent_unchecked`. The operation must be one the aggregated field\'s type\nallows \u2014 `sum` over a number, `earliest` over a date, `filled` over any stored or\nformula field. A lookup is never rolled up: roll up the child\'s own field, or a formula\nover it. A rollup states no `notation`: `sum`, `avg`, `median`, `min`, `max` and `range`\nread in the aggregated figure\'s, a count as a plain number, and a `percent_*` as a\npercent (0\u2013100), in the app as in the table.\n\n### `lookup`\n\nDisplays a field from the linked records.\n\n<!-- generated:start field-lookup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to look up through |\n| `lookup_field_alias` | alias | yes | Alias of the field on the linked entity to display |\n| `order_by` | [Lookup order](#lookup-order) | no | Show ONE linked row\'s value \u2014 the first in this order \u2014 rather than every linked row\'s |\n\n#### Lookup order\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_key` | text | yes | A field alias on the linked entity the rows are ordered by |\n| `direction` | `"asc"` \\| `"desc"` | yes | Which end of that order the one row is taken from |\n\n<!-- generated:end field-lookup -->\n\nInside a formula, a lookup holding one value is that value (`{due_soon}` is\n`true`, not `[true]`); several values are a list.\n\n### `autonumber`\n\n<!-- generated:start field-autonumber -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `prefix` | text | no | Literal prefix prepended to every display value (e.g. \'KH-\' \u2192 \'KH-001\'). Ignored when `template` is set. |\n| `padding` | integer | no | Zero-pad the integer to this width. Default 1 (no padding). 3 \u2192 \'001\', \'012\', \'123\', \'1234\' (overflow uses the actual width). Ignored when `template` is set. |\n| `template` | text | no | Format template with placeholder tokens evaluated at insert time. Tokens: {N} (raw integer), {N:W} (zero-padded to width W, e.g. {N:3} \u2192 001), {YEAR} (4-digit year), {YEAR:2} (2-digit year), {MONTH} (2-digit month), {DAY} (2-digit day). Date tokens use the workspace timezone. Example: \'HM-{YEAR}-{N:3}\' yields \'HM-2026-001\'. Stored as the composed string; subsequent template edits do NOT re-format existing rows (date tokens would lose the original creation date). |\n\n<!-- generated:end field-autonumber -->\n\n<!-- generated:start rules-field -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `field.default` | A default names options its select declares \u2014 one on a single select \u2014 and a date stamped by `derive_from` states none. |\n| `field.default-from` | A field\'s `default_from` reads `<link>.<field>` off the row a one-row link of its entity names \u2014 a link no copy fills \u2014 into a field of the same type, date format and notation that `derive_from` does not stamp: a lookup only through a one-row link, a link only into one to the same entity, a select\'s option as this field\'s of the same alias (one it lacks is not copied), several values only into a field holding several. The server copies it on every add that fills the link and leaves the field empty, so an add sure to fill the link \u2014 a required one, or the record it files the row under \u2014 does not ask it by default. |\n| `field.writes` | A stored field\'s `writes: false` says no person keys it in an app \u2014 a workflow or an integration writes it: every app draws it read, no add (`create`) or act asks it and no paper fills it, and a required one takes a `default` or a `default_from` where an app adds its rows; never on a date `derive_from` stamps, which is read already. |\n| `field.option-mark` | An option\'s `mark` is one the kit draws, and a select\'s options mark every one or none. |\n| `field.unit-select` | A notation\'s `per_row` names a single select of the same row, or a lookup of one through a one-link, whose every option label is a unit, or an ISO 4217 code. |\n| `field.link` | A link targets a declared entity: its `display_field_alias` a field of it, its `paired_field_alias` a link on it naming this one back \u2014 and of two paired links at most one reads one row. |\n| `field.formula` | A formula parses, reads fields of its own entity by alias, names an option as `{field:option}` of one a select declares, and compares a select to its options\' keys, never their words. |\n| `field.formula-type` | A formula yields what it states \u2014 its `output_type`, else a figure where it states a `notation` and text where it states `link` \u2014 as the platform infers it from the expression when the field is created. |\n| `field.rollup` | A rollup aggregates, through a link of its entity, a field of the linked entity by an operation a model declares and that field\'s type takes \u2014 never a figure read in each row\'s own unit or currency \u2014 filtered on fields of the linked entity. |\n| `field.lookup` | A lookup reads, through a link of its entity, a field of the linked entity \u2014 never one read in each row\'s own unit or currency \u2014 ordered by a field of it. |\n| `field.computed-cycle` | Computed fields never wait on each other in a loop: each is computed after what it reads. |\n\n<!-- generated:end rules-field -->\n\n## Views\n\nSaved views live under the entity they belong to. Every field reference is a\nfield ALIAS on that entity.\n\n<!-- generated:start views -->\n\n#### View\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable view alias, unique within the entity |\n| `label` | text | yes | Display name of the view |\n| `description` | text | no | The view\'s description, written onto the view in the workspace |\n| `columns` | list of [View column](#view-column) (at least one) | no | The columns the view shows, in this order and no others; absent, every field |\n| `filters` | [filter](#views) | no | The rows the view keeps |\n| `sort` | [sort](#views) | no | The order the view reads its rows in |\n| `summary` | map of text \u2192 text | no | Field alias \u2192 the operation its footer cell states |\n| `frozen_columns` | integer \\| `null` | no | How many leading columns stay in place while the rest scroll |\n\n#### View column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_alias` | alias | yes | A field of this entity |\n| `visibility` | `"visible"` \\| `"hidden"` | yes | Field visibility state: \'visible\' = shown to everyone, \'hidden\' = not shown by default but members can toggle |\n| `width` | number | no | The column\'s width, in pixels |\n\n<!-- generated:end views -->\n\nA filter is a group \u2014 `{ "node_type": "group", "logic": "and" | "or",\n"children": [ \u2026 ] }` \u2014 or one condition on its own, `{ "node_type":\n"condition", "type": "select", "field_key": "tier", "operator": "has_any_of",\n"value": ["gold"] }`. A sort is a list of `{ "field_key": \u2026, "order": "asc" |\n"desc" | null }`. A condition\'s `type` is the field\'s type and its `operator` is\none that type admits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n<!-- generated:start rules-view -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `view.fields` | A view\'s columns, summary, sort and filters name fields of its entity. |\n\n<!-- generated:end rules-view -->\n\n## Roles\n\nA role becomes a workspace group.\n\n<!-- generated:start roles -->\n\n#### Role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable role alias, bound to a workspace group when the model is applied |\n| `label` | text | yes | The group\'s name in the workspace |\n\n<!-- generated:end roles -->\n\n## Templates\n\nAn `html` or `email` template carries its content inline, and `{{name}}` in it is filled\nfrom the workflow\'s data; an `excel` template names an uploaded workbook by its `file_id`,\nfilled with the register\'s report by its `export` alone.\n\nInline content is refused where it holds a script, a frame, a `<link>`, an inline event handler or a\n`javascript:` URL; an `html` one may be a fragment, kept as written and printed as a whole document. An apply brings a\ntemplate it made before to the content the model states, and leaves one it adopted by its label as\nits owner wrote it.\n\n<!-- generated:start templates -->\n\n#### Template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"html"` \\| `"email"` \\| `"excel"` | yes | html is a page a workflow renders to a PDF; email is a message a workflow sends; excel is a workbook a register\'s export fills |\n| `alias` | alias | yes | Stable template alias, unique within the contract |\n| `label` | text | yes | The template\'s name in the workspace |\n\n<!-- generated:end templates -->\n\n### `html` and `email`\n\n<!-- generated:start template-inline -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `content` | text | yes | Inline template content; placeholders reference field aliases |\n| `content_sha256` | text | no | sha256 (64-char lowercase hex) of the utf-8 content; derived where the template is written when absent |\n| `locale` | `"vi"` \\| `"en"` | no | The language its figures, dates and amounts in words print in; absent prints in the organization\'s |\n\n<!-- generated:end template-inline -->\n\n### `excel`\n\n<!-- generated:start template-excel -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `file_id` | text | yes | The uploaded .xlsx (`fil_\u2026`) the template is made from; its markers read what the export fills it with |\n\n<!-- generated:end template-excel -->\n\nA paper that has to look like a counterparty produced it \u2014 an official letter,\nan acceptance minute, a supplier\'s bill \u2014 is the same `html` template with a\nshell around the body: a letterhead, a reference line, a seal and a signature\nblock, and paper grain over everything. One shell, many bodies; the data is the\nonly thing that changes, so a workflow can re-issue it over any record.\n\n```jsonc\n{ "alias": "cong_van", "label": "C\xF4ng v\u0103n", "type": "html",\n "content": "\u2026the page below, as one JSON string\u2026" }\n```\n\n```html\n<style>\n .sheet{position:relative;width:718px;padding:44px 58px 30px;background:#fbfaf6;color:#111;font:14.2px/1.5 \'Liberation Serif\',serif}\n .grain{position:absolute;inset:0;opacity:.34;mix-blend-mode:multiply;background:url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'140\' height=\'140\'><filter id=\'f\'><feTurbulence baseFrequency=\'.9\' numOctaves=\'2\'/><feColorMatrix values=\'0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .35 0\'/></filter><rect width=\'140\' height=\'140\' filter=\'url(%23f)\'/></svg>")}\n .top{display:flex;text-align:center;font-size:13.4px} .top>div{flex:1} .u{display:inline-block;border-bottom:1px solid #111;font-weight:700}\n .ref{display:flex;text-align:center;font-size:13.4px;margin-top:6px} .ref>div{flex:1} .ref .r{font-style:italic}\n h1{text-align:center;font-size:15.6px;margin:26px 0 18px} p{text-align:justify;text-indent:26px;margin:0 0 9px}\n .sig{display:flex;margin-top:20px} .sig .l{flex:1} .sig .r{width:290px;text-align:center;position:relative}\n .sig .nm{font-weight:700;margin-top:96px} .seal{position:absolute;left:4px;top:8px;width:166px;height:166px;opacity:.66;mix-blend-mode:multiply;transform:rotate(-17deg)}\n </style>\n <div class=\'sheet\'><div class=\'grain\'></div>\n <div class=\'top\'><div><b>{{issuer_parent}}</b><br><span class=\'u\'>{{issuer}}</span></div>\n <div><b>C\u1ED8NG H\xD2A X\xC3 H\u1ED8I CH\u1EE6 NGH\u0128A VI\u1EC6T NAM</b><br><span class=\'u\'>\u0110\u1ED9c l\u1EADp - T\u1EF1 do - H\u1EA1nh ph\xFAc</span></div></div>\n <div class=\'ref\'><div>S\u1ED1: {{number}}</div><div class=\'r\'>{{place}}, ng\xE0y {{day}} th\xE1ng {{month}} n\u0103m {{year}}</div></div>\n <h1>{{title}}</h1>\n <p>K\xEDnh g\u1EEDi: {{recipient}}.</p>\n {{{body}}}\n <div class=\'sig\'><div class=\'l\'><b>N\u01A1i nh\u1EADn:</b><br>- Nh\u01B0 tr\xEAn;<br>- L\u01B0u VT.</div>\n <div class=\'r\'><img class=\'seal\' src=\'{{seal_url}}\'><b>{{signer_title}}</b><div class=\'nm\'>{{signer}}</div></div></div>\n </div>\n```\n\n**What an act\'s template is handed** is its record\'s own fields by alias: a\nselect, a member and a link as their words (a link as the linked row\'s title), a\nnumber, a date and a computed value that declares its kind raw (`2800000`,\n`2026-09-14`) \u2014 never a files field or a computed select. On one record it is\nhanded, too, the rows of each child the record draws as `rows` whose alias the\ntemplate names, listed under that alias for `{{#each <alias>}}` in place of the\nrecord\'s own link to them: every row filed under the record, oldest first, each\nby its own fields as the record\'s are, less its link back to the record. Over\nseveral rows the rows are listed under the entity\'s alias, for\n`{{#each <alias>}}`, without their child rows.\n\nHelpers print a raw value in the template\'s `locale` \u2014 `vi` or `en`, the organization\'s language where it states\nnone; each prints `""` for an empty value:\n\n| Helper | Prints (`vi` / `en`) |\n|---|---|\n| `{{money total "USD"}}` | `1.250,50 US$` / `$1,250.50`, in the currency\'s minor units; no code, a number |\n| `{{number qty}}` | `1.234,5` / `1,234.5`: grouped, up to 2 decimals |\n| `{{date issued_on}}` | `14/09/2026`; a date-fns pattern as the second argument, `{{date at "dd/MM/yyyy HH:mm"}}`, literal text quoted (`"\'Ng\xE0y\' dd"`). A moment prints on the workspace\'s timezone |\n| `{{words total "USD"}}` | `M\u1ED9t ngh\xECn hai tr\u0103m n\u0103m m\u01B0\u01A1i \u0111\xF4 la M\u1EF9 n\u0103m m\u01B0\u01A1i xu` / `One thousand two hundred fifty US dollars and fifty cents`; no code, VND (`\u0111\u1ED3ng`) |\n\nA value it cannot read (`{{money "abc"}}`, `{{date "14/09/2026"}}`) fails the render. A field named like a\nhelper and written alone (`{{number}}`) prints its own value.\n\n## Rows\n\nFirst records, keyed by entity alias. Up to 200 rows per entity and 2000 across\nthe model, attaching at most 2000 documents between them \u2014 a real data set\nbelongs in an import, not a model.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n<!-- generated:start rows -->\n\n#### Row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | text | yes | Local handle for this row, referenced by other rows\' link fields |\n| `fields` | map of text \u2192 any value | yes | Field alias \u2192 the value, read against the field\'s declared type |\n\n<!-- generated:end rows -->\n\n<!-- generated:start rules-rows -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `rows.ref` | A row\'s `ref` is unique within its entity. |\n| `rows.required` | A row states every field its entity requires: it is created with what it states, its fields\' defaults and copies unapplied. |\n| `rows.distinct` | Open rows of an entity a register lists read apart: no two state the same title and the same line under it, and an autonumber on the line reads every row apart. |\n| `rows.value` | A row\'s value fits its field: an option alias for a select, `"<entity>:<ref>"` of a row of the linked entity in this file for a link, `"self"` for a member, a relative path beside the file or a `fil_` id for files, a date in the date grammar (an hour only on a datetime), a text, number, yes/no or null otherwise \u2014 and none on a computed field. |\n\n<!-- generated:end rules-rows -->\n\nA `ref` is lowercase letters, digits and underscores, and is never persisted.\n\nA `files` cell attaches documents: paths relative to this file (no `..`, never\nabsolute), which the check proves exist and `apply` uploads into the workspace\nbefore any row is written \u2014 a paperwork business seeds its papers with its\nrows. The server accepts only `fil_` ids of files this workspace owns, which is\nwhat the upload leaves behind. After a run that wrote rows, the WORKSPACE holds\nwhich record each row became, under the row\'s own `<entity>:<ref>`:\n`delete_records` over them is how a seeded set is reset, and applying again\nre-dates it.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `select_record_link` | `"<entity-alias>:<ref>"` naming another row in this file \u2014 `"customer:acme"`, or an array for several; a paired link is stated on ONE side (the child\'s link to its parent) and its partner fills itself |\n| `select_member` | `"self"` only \u2014 the person applying the model |\n| `files` | paths beside this file \u2014 `["scans/pccc_letter.png"]` \u2014 uploaded by `model apply`/`setup` before the model is sent; or `fil_` ids of files already in this workspace |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe model is applied, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n- on a date that holds its hour (`format: "datetime"`), that hour after the day, and always stated:\n `@today 14:30`, `@today+1 06:00`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Records\n\n`records` says how a row of each entity is RECOGNISED \u2014 once per entity, keyed by\nentity alias \u2014 and every surface that draws one of its rows reads the same\nstatement: the register\'s row, a picker\'s option, a link\'s chip, a child table\'s\nrow and the record\'s header. Every entity an app lists, opens or picks has one.\n\n```jsonc\n"records": {\n "visit": {\n "title": "container_no",\n "subtitle": ["customer", "arrived_on"],\n "image": "photos",\n "status": { "field": "stage", "closed": ["gone"], "history": "visit_history" },\n "figure": "total_fees",\n "due": ["free_until"]\n },\n // A release order read against what it allows: a meter wherever it shows.\n "release": { "title": "release_no", "figure": "issued", "limits": { "issued": "units" } },\n // A dossier\'s stage is the last date it reached \u2014 no stored select.\n "dossier": { "title": "applicant", "status": { "milestones": ["received_on", { "field": "appraised_on", "when": { "kind": ["loan"] } }, "signed_on"], "closed": ["signed_on"] } },\n // A site owes a diary page each day it works, from breaking ground through handover.\n "site": { "title": "name", "cadence": [{ "entity": "diary_page", "on": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "started", "until": "handed_over" }] }\n}\n```\n\n<!-- generated:start records -->\n\n#### Record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | alias | yes | The field that names a row \u2014 the first thing every surface shows: who or what it is, a link naming it by the linked row (a job read by its customer). An autonumber is refused; a typed number is the subtitle |\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title wherever a row is drawn (a code, a date, a party): one line at a desk, wrapping to a second on a phone |\n| `image` | alias | no | A files field that pictures the row \u2014 never one an act keeps the document it makes in |\n| `mark` | alias | no | A single select whose option leads the row wherever a mark leads it \u2014 a list\'s row, the record\'s head, a link naming the row: the option\'s brand, else its colour dot, on a tile. The field stays a field: a subtitle run, a column, a section\'s control. Never beside `image` or `party`, and never the status field |\n| `description` | alias | no | A text field saying what the row is about \u2014 a plan\'s brief, a customer\'s note: read in its record\'s head under the title\'s line, its first lines at rest and whole on a press, and written from the head\'s \u270E. Never on a row\'s line |\n| `party` | `"person"` \\| `"organization"` | no | What each row is where the rows are people (a contact, a patient) or organisations (a customer, a supplier, a carrier); absent, the rows are things (an order, an item, a paper) |\n| `status` | [Status](#status) | no | The stage a row moves through \u2014 a single select or its milestone dates \u2014 drawn as a badge beside its title |\n| `figure` | alias | no | The one number the row stands for, read at its right (a total, a quantity left) |\n| `limits` | map of alias \u2192 (alias \\| number) | no | A number read against a bound \u2014 a field of the same row or a constant \u2014 drawn as a meter wherever it shows |\n| `gates` | map of alias \u2192 (alias \\| number) | no | A pass mark on a meter `limits` bounds \u2014 a field of the same row or a constant; a percent field is its share of the bound, any other number an amount. The meter marks it and fills complete once past it |\n| `tolerance` | map of alias \u2192 number | no | How far past its `limits` bound a meter may stand, in percent of the bound: the bound is an amount expected (received against ordered), not a cap. Without one the bound is a cap, and past it is an overrun |\n| `due` | list of alias (at least one) | no | Dates that are deadlines (stored, or computed as a date): each counts down and turns overdue |\n| `frees` | alias | no | A date ending the row\'s span of days on which what the row holds is free again (a check-out, a hire\'s return): a stay from the 3rd to the 5th holds the 3rd and the 4th, and another may start on the 5th \u2014 on a lanes board, a calendar, a roster and in `no_overlap` alike. Absent, a span of days holds its last day; a span of moments always frees its end |\n| `settled` | alias | no | A yes/no of the row (stored, or a formula: an invoice paid in full) that ends its deadlines: while it is true, no `due` of the row counts down or turns overdue \u2014 in a cell, on its line, in its head, on a calendar or gantt, or in the overdue count. A dashboard\'s or reading\'s `where` reads it as any other yes/no |\n| `task` | `true` | no | A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record\'s rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed` |\n| `cadence` | list of [Cadence](#cadence) (at least one) | no | The rows of a child each row owes one of a day, on the days of the week listed (a site\'s diary page, Monday to Saturday) \u2014 one entry a child, never owed on a day still to come. A roster of these rows over days marks a past day owed with none missing in the danger tone, counted in the legend; today\'s is due, offered to add and never marked; each row\'s total reads the days it holds of those owed |\n| `applies` | map of alias \u2192 map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Fields that apply only while their conditions hold (a length only on a line whose tariff charges by the metre): where one does not apply no add asks it and no surface shows it. Never the title; a required one starts at its `default` |\n| `starts` | map of alias \u2192 `"today"` \\| `"me"` | no | What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default` |\n| `scan` | `"text"` \\| `"iso6346"` | no | Its title, a text field, is painted or printed on the thing a row is (a container\'s number, a seal\'s) and read off one photo on a phone, beside each search matching it and each field holding or picking it: `iso6346` a container number, refused unless its check digit holds; `text` as it stands |\n| `place` | [Place](#place) | no | Where a row stands, as plain fields of it, drawn as one place where its record draws the address: how sure it is, a press opening it in the device\'s maps, and one taking it from where the device stands |\n| `on_site` | [On site](#on-site) | no | Photos taken where a place stands (a site diary\'s): a photo taken through a phone\'s camera here keeps where the device stood, marked at the place, away from it or unknown, and a record holding one taken away says so among its checks \u2014 one with no place (picked on a computer, or kept before places were) is marked and warns nothing; a place reaches only members of the organization, through the apps drawing these photos |\n| `pin` | [Pin](#pin) | no | Each row is a place on a picture (a spot on a container\'s drawing, a room on a plan): a record listing rows that link to these by a single link draws that picture over them, a numbered pin at each listed row\'s place, each row led by its number, the row opened lit on it and a pin pressed opening its row |\n\n#### Cadence\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | The child entity owed: each of its rows stands on one row of this entity by its single link to it, and on the first date of its line (`records` title or subtitle) |\n| `on` | list of `"mon"` \\| `"tue"` \\| `"wed"` \\| `"thu"` \\| `"fri"` \\| `"sat"` \\| `"sun"` (at least one) | yes | The days of the week one is owed on, each listed \u2014 `mon` to `sun` |\n| `from` | alias | no | A date of this row: the first day one is owed (a site breaking ground, a contract\'s start); absent, no first day |\n| `until` | alias | no | A date of this row: the last day one is owed (a site\'s handover, a contract\'s end); empty or absent, through today |\n\n#### Place\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `address` | alias | yes | A text field: the address, as people write it |\n| `latitude` | alias | yes | A number field: degrees north, negative south |\n| `longitude` | alias | yes | A number field: degrees east, negative west |\n| `accuracy` | alias | no | A number field: how many metres off the device that gave the place said it may be |\n\n#### On site\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photos` | list of alias (at least one) | yes | Files fields of this row holding photos taken on site |\n| `place` | alias | no | A single link of this row to the row whose `place` its photos are taken at (a diary page\'s site); absent, this row\'s own `place` |\n| `radius` | number | yes | How many metres from the place a photo still counts as taken there |\n\n#### Pin\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `on` | alias | yes | A single link of this row to the row whose picture (its `records` `image`) the place is on \u2014 a container\'s drawing, a floor\'s plan |\n| `x` | alias | yes | A number field of this row: how far across that picture the place stands, in percent from its left edge |\n| `y` | alias | yes | A number field of this row: how far down that picture the place stands, in percent from its top edge |\n\n<!-- generated:end records -->\n\n<!-- generated:start rules-records -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.title` | A title names the row by what it is \u2014 never an autonumber; a code is its subtitle, and the subtitle never names the title\'s own field. |\n| `records.image` | `image` is a files field, never one an act keeps the documents it makes in (`into`). |\n| `records.mark` | `mark` is a single select, never the status field, and never beside `image` or `party` \u2014 a list keeps one lead slot \u2014 nor on the folder an app works inside (`scope`), which its switcher names by a picture or its initials. |\n| `records.description` | `description` is a text field, read in the record\'s head \u2014 never a section\'s too. |\n| `records.figure` | A figure is a number. |\n| `records.applies` | `applies` holds a field of the row other than its title to conditions of the same row \u2014 a yes/no (stored or a formula) by `true` or `false`, a single select by its options, its own or looked up through a one-row link \u2014 never to itself; a required field it holds starts at a `default`. |\n| `records.limits` | `limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit. |\n| `records.gates` | `gates` marks a pass on a meter `limits` bounds: a percent field (its share of the bound), another number in the bound\'s unit, or a constant. |\n| `records.tolerance` | `tolerance` stands on a meter `limits` bounds. |\n| `records.starts` | `starts` starts a field a person writes \u2014 `today` a date or a moment (never a span), `me` a member \u2014 and an add that does not ask a field it starts writes that start. |\n| `records.due` | A deadline (`due`) is a date. |\n| `records.frees` | `frees` names a stored day, never a moment: a span of moments already frees its end. |\n| `records.settled` | `settled` names a yes/no of the row (stored, or a formula) beside a `due`: while it holds, the row\'s deadlines neither count down nor turn overdue. |\n| `records.task` | A task entity (`task`) states a `status` of a stored single select with `closed` options \u2014 its ring ticks a row done by moving it to one. |\n| `records.cadence` | A `cadence` names each child once \u2014 its rows standing on one row of this entity by their single link and on the first date of their line \u2014 lists each day of the week once, and `from` and `until` are two different days of this entity. |\n| `records.scan` | A title read off a photo (`scan`) is a text field the row stores. |\n| `records.place` | A `place` names four different fields of the row: its address a text field, its latitude, longitude and accuracy number fields it stores. |\n| `records.on_site` | `on_site` lists files fields of the row, each once, taken at the `place` of the row itself or of the one row a single link of it names (`place`), which states one. |\n| `records.pin` | A `pin` names a single link of the row (`on`) to an entity whose `records` states its `image`, and two different number fields of the row, how far across (`x`) and down (`y`) that picture in percent. |\n| `records.picture` | *Refused until adopted or declined:* Rows an app draws that each hold one files field no act makes, no call is filed into, no files block keeps and no list compares as a pair (`compare`) \u2014 and no figure \u2014 are pictured by it (`image`); where the workspace\'s files are known, the field holding images is the picture, a figure or not. |\n\n<!-- generated:end rules-records -->\n\n- **The title names the row** by who or what it is \u2014 a link by the linked row (a job\n read by its customer). An autonumber is refused as a title; a typed number is the\n subtitle.\n- **One lead per row.** A row leads with its picture (`image`), a face (`party`) or the\n option of a single select it holds (`mark`) \u2014 never two. A `mark` draws that option\n on a tile: the brand it is, else its colour dot; a row holding none, a ring saying\n none is picked. The same tile heads the row\'s record and leads it where a link\n names it; the select stays a field \u2014 a subtitle run, a column, a section\'s control.\n- **A limit** reads a number against a bound \u2014 another field of the row, or a constant.\n A `gates` entry marks a pass on that meter: a percent field is its share of the\n bound, any other number an amount.\n- **A deadline** (`due`) counts down wherever it is drawn and is overdue once past while\n the work is open. A register over the entity says how many rows in view are overdue on\n its summary line, and pressing that count narrows to them \u2014 no reading to state.\n- **A span\'s free day** (`frees`) is the end date on which what the row held is free\n again \u2014 a check-out, a hire\'s return. A stay from the 3rd to the 5th holds two nights:\n a lanes board, a calendar and a roster draw it through the 4th, and `no_overlap` lets\n the next stay start on the 5th. Without it a span of days holds its last day; a span of\n moments always ends as the next may begin.\n- **`starts`** is where an add\'s field begins, stated only where that is certain \u2014 a\n log\'s own day at `today`, a request\'s requester at `me`. Nothing else starts a field\n but its declared `default` and what the reader narrowed the register to.\n\n## Status\n\nA record\'s `status` is the stage a row moves through \u2014 a stored single select, or the\nmilestone dates it reaches \u2014 drawn as a badge beside its title wherever the row is.\n\n<!-- generated:start status -->\n\n#### Status\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | no | The single select that is the row\'s state \u2014 or `milestones` instead |\n| `milestones` | list of (alias \\| [Milestone](#milestone)) (at least 2) | no | Dates in the order the work reaches them, instead of a stored select: the stage is the last one filled, ticking one stamps today, and clearing one clears every later one |\n| `closed` | list of alias (at least one) | no | Options (or milestones) that end the work: a register opens on the rows not at them, and the badge reads muted |\n| `history` | alias | no | A child entity every status change appends one row to (its link to this entity, the new status, the moment and who) \u2014 written by the app\'s own writes, never a table automation; a row the model seeds opens it at its status, the day the rows land, unless the model states its rows; with `field` only |\n| `stages` | map of alias \u2192 [Stage](#stage) | no | Option alias \u2192 who owns that stage and how long it should take: the owner alone presses the acts offered there, refused on the server for anyone else. The status then moves only by acts that `set` one of its options \u2014 no save writes it and no act asks it; with `field` only |\n\n#### Milestone\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A date field: the stage is reached on the day it holds |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | yes | The stage applies only while each named select holds one of these options, and each named stored yes/no is this value; otherwise the row skips it |\n\n#### Stage\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `owner` | text \\| [Stage role](#stage-role) | yes | Who owns the stage: a member field of the row, `link.field` read through a one-row link, or `{ role }` \u2014 a group |\n| `within` | integer | no | Whole days the stage should take from the moment the row entered it |\n\n#### Stage role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `role` | alias | yes | A role of the model\'s `roles`: every member of the group it binds to owns the stage |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.status` | A status is one single select, `closed` naming options of it \u2014 or its `milestones`. |\n| `records.history` | A declared status `history` has exactly one link to the row it records, one single select holding every option of the status (or a text), exactly one date and at most one member. |\n| `records.history-derived` | A `history` the model does not declare is derived and keeps one entity\'s moves, labelled by that entity ("L\u1ECBch s\u1EED <entity label>", or "<entity label> history"): no declared table holds that label, and the entity has no other field under the link it adds. |\n| `records.history-scope` | A status history reads by its record\'s `read_scope`, answered through its one-row link to the record within the links a row rule reaches \u2014 or it states its own. |\n| `records.milestones` | Milestones are dates a person ticks, each named once; `closed` names some of them, and a deadline (`due`) is never one. |\n| `records.milestone-add` | An add asks the first milestone at most \u2014 each after it is ticked in order. |\n| `records.stages` | A stage\'s owner (`status.stages`) is a single member field of the row, one read through a one-row link, or a declared role, on an option that does not end the work; a role\'s stage needs the status `history` with who moved the row. A status with stages moves only by acts that `set` one of its options \u2014 no save writes it, and no act asks it, sets it otherwise or writes it from a workflow \u2014 and the moment its row entered its stage is kept in `<status>_since`, derived beside it, which nothing else writes. |\n| `records.history-note` | *Noted, never refused:* A record, never a task, whose stored status an act of its app moves, with no `history`: when a row entered each status and who moved it is kept nowhere. |\n| `records.detour-note` | *Noted, never refused:* A stage the work only steps aside into \u2014 not where a row starts, entered from one step alone that also moves the work on without it, and left by its own moves only back to that step (a pause, a repair) \u2014 is a detour: its option and the section staged at it stand before that step\'s, else the work reads it as its next step, and the act moving the work into it leads that section \u2014 never a correction waiting in a \u22EF. |\n\n<!-- generated:end status -->\n\n- **The status** is one single select. `closed` options end the work: a register opens\n on the rows not in them. A `history` entity gets one row per move \u2014 its link to this\n entity, the new status (a select holding the same option aliases, or text), the\n moment and who \u2014 appended by the app\'s own writes that move the status, never by a\n table automation. A `history` the model does not declare is derived, and needs no\n `records` entry of its own.\n- **Milestones** are the alternative to a stored select: dates in the order the work\n reaches them, the stage being the last one filled (never stored). A milestone with a\n `when` applies only while its select holds those options and its stored yes/no that\n value; otherwise the row skips it.\n The record draws them as one block where ticking stamps today and clearing one clears\n every later one; every save and act re-checks that a date comes after each earlier one\n that applies. `closed` names the milestones that end the work. A register\'s add may ask\n the first milestone, never a later one; an act may stamp one, never clear it.\n\n## Write rules\n\n`write_rules` is what a WRITE meets, keyed by entity alias; each entity\'s is held\nby its table. Every generated write of an\napp (`create_<entity>`, `update_<entity>`, each act) re-checks these on the\nserver; the screen only mirrors them.\n\n```jsonc\n"write_rules": {\n // A customer is RECOGNISED by their address. An add that names one \u2014 from the\n // customers register or from a picker\'s "new" \u2014 reuses the row it matches and\n // opens one only where nothing does, so the book never grows a second Acme.\n "customer": { "natural_key": ["email"] },\n "order_line": {\n "fields": {\n // A line of nothing is not a line. Refused on create and on update.\n "quantity": { "min": 1, "max": 9999 },\n // Shipped inside its order\'s window: a bound read off the row a link names\n // is held from both sides \u2014 moving the order\'s dates never strands a line.\n "ships_on": { "min": "order.placed_on", "max": "order.due_on" },\n // Nothing is sold off an empty shelf. The picker reads only the rows that\n // answer this, and the write refuses the same rows again.\n "product": {\n "options_where": {\n "node_type": "group", "logic": "and",\n "children": [{ "node_type": "condition", "type": "number",\n "field_key": "in_stock", "operator": "greater_than", "value": 0 }]\n }\n }\n }\n },\n // One booking per room at a time, while it is held: a create or a save whose\n // span overlaps another held booking of the same room is refused.\n "booking": {\n "fields": { "ends_on": { "min": "starts_on" } },\n "no_overlap": { "from": "starts_on", "to": "ends_on", "by": ["room"], "while": { "state": ["held"] } }\n }\n}\n```\n\n<!-- generated:start write-rules -->\n\n#### Entity write rules\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `natural_key` | list of alias (at least one) | no | The field aliases a row of this entity is RECOGNISED by. A write that names a row of this entity by these values reuses the row it finds, minting one only where nothing matches. |\n| `fields` | map of alias \u2192 [Field write rule](#field-write-rule) | no | Field alias \u2192 what that field\'s value is bounded by or picked among |\n| `no_overlap` | [No overlap](#no-overlap) | no | No two rows hold overlapping spans \u2014 a create or update that would is refused |\n\n#### Field write rule\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (its notation\'s `per_row`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n| `options_where` | [filter](#views) | no | Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET entity\'s own fields, each `field_key` a field alias. It narrows the picker\'s read and is refused again where the write lands. |\n| `same` | list of alias (at least one) | no | One-row links this row and each row this link points at must name alike \u2014 a fee\'s invoice is one of its own visit\'s, a box\'s seal one of its own shipping line\'s: each a one-row link of both entities to the same entity. It narrows the picker\'s read to the rows naming what this row names, and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare. |\n| `suggest` | text | no | A text field whose control offers, as the reader types, the distinct values a text field of a catalog holds \u2014 `<entity alias>.<field alias>` (a damage position\'s code among the codes on file); any text is still written |\n\n#### No overlap\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The stored date (or datetime) a row\'s span starts on |\n| `to` | alias | yes | The stored date (or datetime) a row\'s span ends on \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at |\n| `by` | list of alias (at least one) | no | Fields two rows share to compete for a span (the same employee, the same room); absent, every row |\n| `while` | map of alias \u2192 list of alias (at least one) | no | Only rows at these options of these selects hold their span (running, not closed); absent, every row |\n\n<!-- generated:end write-rules -->\n\n<!-- generated:start rules-write -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `write.natural-key` | A `natural_key` is text or a number a person types back, or a one-row link; a text key is unique \u2014 `unique: true`, or the key\'s fields as a set in the entity\'s `unique`. |\n| `write.bounds` | `min` and `max` bound a number or a date \u2014 by a constant a number, by a field of this row or of the one row a one-row link names either, of the same type and unit, never by itself \u2014 and `min` is never above `max`. |\n| `write.bound-per-row-unit` | On a figure whose unit or currency is the row\'s own (its notation\'s `per_row`) a constant bound is only 0 \u2014 bound it by a field of the row. |\n| `write.suggest` | `suggest` stands on a stored text field and names `<entity>.<field>`, a stored text field of a declared entity; the app reads that entity, so it has a `records` entry. |\n| `write.same` | `same` stands on a link and names one-row links this entity and the link\'s target both hold to one entity. |\n| `write.options-where` | `options_where` narrows a link by an `and` group of plain conditions over the target\'s own fields \u2014 a number compared in one unit on every row, a select by options it declares. |\n| `write.no-overlap` | `no_overlap` spans two date fields; `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options. |\n\n<!-- generated:end rules-write -->\n\n- A `natural_key` is `text` or `number`, because a person types it back, or a\n one-row link, because a row is also recognised by the one row it belongs with (a\n fee by its visit and its kind). A key holding a `text` field is unique: `unique:\n true` on that field, or the key\'s fields as a set in the entity\'s `unique` (a\n site by its customer and its name) \u2014 two rows sharing it would make\n find-or-create pick whichever the read answered first. An add asks the key by\n default.\n- A `min` or `max` naming a field is read off the row as the save would leave it,\n and one naming `<link>.<field>` off the row the link names \u2014 the link\'s side\n re-checks its own rows when that bound moves.\n- A `same` link names only rows that name what this row names \u2014 a fee\'s invoice\n is one of its own visit\'s \u2014 through a one-row link both entities hold to one\n entity; its picker offers nothing until the row names one. A row added under a\n record names what it copies off it (`default_from`) from the start, so a block\n expecting one line per row of the link lists that record\'s rows alone. A save\n moving what the two compare, on either row, is refused while they would name apart.\n- An `options_where` link may be left empty; a row it names is refused again\n where the write lands unless it holds the narrowing. A link naming one kind of\n a book\'s rows (a "Shipping line" among the parties) narrows to that kind, or\n its picker offers every row.\n\nA field\'s own `required` is not here: the contract carries it, and every write\npath refuses the row by field name from it. `unique` is the text field\'s own\nclause (\xA7 `text`) \u2014 an add says so at the control before the column does. A\nfield the workspace writes (a formula, a rollup, a lookup, an autonumber, a date\nwith `derive_from`) is never asked, written or edited.\n\n## Apps\n\nAn app is ONE register over one entity and the record each row opens \u2014 or a dashboard\nof readings. The author states COMPOSITION \u2014 which fields go where; the runtime owns\nhow each thing looks, one treatment per concept. Nothing is drawn that the app does not\nname, and each fact appears once on a record. What the vocabulary has no word for is an\nact\'s own `workflow`, or a workflow the app declares beside its acts (`workflows`).\n\n```jsonc\n"apps": [{\n "alias": "gate", "name": "Gate in and out", "entity": "visit",\n "register": { "columns": ["service"], "filters": ["customer", "service"] },\n "record": {\n "sections": [\n { "title": "In", "fields": ["customer", "service"], "blocks": [{ "rows": "fee", "columns": ["amount"] }] },\n { "title": "Out", "at": ["in_yard"], "fields": ["release", "seal"], "blocks": [{ "files": ["photos"] }], "acts": ["gate_out"] }\n ]\n },\n "acts": [{ "alias": "gate_out", "label": "Gate out", "when": { "stage": ["in_yard"] },\n "requires": ["release", "seal"], "set": { "stage": "gone", "left_on": "now" }, "confirm": true }],\n "checks": [{ "field": "unpaid", "blocks": ["gate_out"] }]\n}]\n```\n\nThe keys below are the app\'s own. Each part it composes is a page of its own: `model/register`,\n`model/layouts`, `model/tabs`, `model/export`, `model/readings`, `model/averages-and-bands`, `model/dashboards`, `model/record-page`,\n`model/blocks`, `model/composed-page`, `model/publish-desk`, `model/timeline-and-agenda`, `model/statements`, `model/reading-blocks`, `model/tasks`, `model/acts`,\n`model/papers-and-recordings`, `model/checks`.\n\n<!-- generated:start apps -->\n\n#### App\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `entity` | alias | yes | The entity whose rows this job works \u2014 the register\'s rows |\n| `books` | list of [Book](#book) (at least one) | no | Other entities the register reads as one list with `entity`\'s rows, each opened, edited and acted on in its own table: the same app over each, its fields lined up with `entity`\'s. Only a table or cards |\n| `scope` | alias | no | A required one-row link of the entity: the app works inside one row of the linked entity at a time (a project, a branch), picked from a list of them, and every row it reads, counts and adds is that row\'s |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `writes` | `"children"` | no | The reader adds and edits the record\'s child rows but not the record\'s own fields; the app\'s acts still move it |\n| `row` | [Row display](#row-display) | no | How this job reads the entity\'s rows where it reads them otherwise than `records` (a cashier\'s figure is what is owed): the subtitle, figure and image stated replace the entity\'s wherever this app draws its rows \u2014 the register, a card, the record\'s header, a calendar, lane or roster entry. The title stays the entity\'s |\n| `register` | [Register](#register) | no | The rows: which columns and filters, the order, how a row is added |\n| `record` | [Record page](#record-page) | no | What a row opens: the door, its sections, and the Activity its head opens \u2014 its saves, and its comments where stated |\n| `acts` | list of [Act](#act) | no | What the reader does to a record \u2014 each a press with its conditions and its write |\n| `checks` | list of [Check](#check) | no | Formulas that warn while they stand, or refuse the acts and saves they name |\n| `workflows` | map of alias \u2192 [Workflow](#workflow) | no | Workflows of the app no act runs, by alias \u2014 called through the app (`run_app_workflow`, its API) by an agent or a service: each states what it takes and hands back, and its body is set on the app (`set_app_workflow`) and kept by every apply. The screen draws nothing for one |\n| `declines` | map of text \u2192 text | no | Decided rules this app stays outside of, each with why in a line: by id, and `records.picture:<entity>` and `model.template-act:<template>` with what they fire on. Read by the apply\'s check alone, never by the app |\n\n#### Theme\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `color` | [colour](#select) | yes | The tile\'s colour |\n\n#### Row display\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title in this app, in place of the entity\'s |\n| `figure` | alias | no | The one number a row stands for in this app, in place of the entity\'s; a `limits` bound on it reads as its meter |\n| `image` | alias | no | A files field that pictures a row in this app, in place of the entity\'s |\n\n#### Book\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | Another entity whose rows the register reads beside its own \u2014 the same job over another table (a legacy ledger, a second branch\'s book) |\n| `fields` | map of alias \u2192 alias | no | A field of the app\'s entity \u2192 the field of this entity holding the same fact, where their aliases differ; a field the app reads lines up by its own alias otherwise, and one this entity lacks is blank on its rows and drops from what they open \u2014 an act, check or add needing it is not offered on them. Options line up by alias |\n\n<!-- generated:end apps -->\n\n<!-- generated:start rules-app -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.alias-unique` | An app\'s alias is unique among the model\'s apps, and an act\'s among its app\'s acts. |\n| `app.icon` | An app\'s `icon` is a Lucide icon name in kebab-case, as `receipt` or `trending-up` are. |\n| `app.records-entry` | Every entity an app lists, opens or picks \u2014 its own, a block\'s, a linked row\'s, a document\'s \u2014 has a `records` entry. |\n| `app.reads-shared` | `reads: "shared"` stands only where a table the app reads states a `read_scope`. |\n| `app.writes-children` | An app that writes only the record\'s children (`writes: "children"`) adds no row of its own entity. |\n| `app.scope` | `scope` is a required one-row link of the app\'s entity, the same on every row the app lists \u2014 never a column, filter, search field, group, `where`, `opens`, section field or add question. |\n| `app.when` | A `when` holds single selects to options and yes/nos to `true` or `false`, stored or a formula of one (a formula with `options` is a select) \u2014 a milestone\'s `when` only a stored select or yes/no, read before any formula over it is computed. |\n| `app.asked-written` | What an add, an act or `starts` fills is a field a person writes \u2014 never a formula, rollup, lookup, autonumber or a date the workspace stamps. |\n| `app.own-rows` | A many-link to rows that each belong to one row of this entity is never a section\'s field, an add\'s question or an act\'s ask: those rows are the record\'s own, read as a `rows` block. |\n| `app.create-asks` | An add asks fields of a table people write \u2014 never the link or the folder it files the row under, which the add sets itself. |\n| `app.create-required` | An add (`create`) asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add. |\n| `app.books` | A register app\'s `books` each name another entity once \u2014 never the app\'s own \u2014 whose fields line up with the app\'s by alias, else by `fields` (a field the app reads, or one a report prints \u2192 one of the book\'s): read as one column type (a day apart from a moment), one value or several alike, a link to the same entity. Each book holds the title, the status, the folder (`scope`), what `where` keeps and every check locking the rows; the register is a table or cards. Two books holding a field of their own under one alias hold it as one kind of value. A report\'s `books` each name the app\'s entity or one of its books, once, and one of them holds each field its `where` and `per` narrow by. `register.book` reads a register that has books. |\n| `app.book-option-kin` | *Noted, never refused:* A book\'s option labelled as one of the app\'s, under another alias, reads apart from it: the register lists both and counts each on its own. |\n| `app.book-lacks` | *Noted, never refused:* A book lacking a field the app\'s record, an act or an add reads: what reads it drops from the book\'s rows \u2014 an act, a block, a section, the add. A field only a report prints, held by a book as another kind of value, prints blank on its rows. |\n| `app.row` | An app\'s `row` states only what differs from `records`: a line that is not the title, a number for its figure, a files picture no act makes documents into. |\n| `app.files-shown` | Every files field is drawn by some app over its entity \u2014 a section\'s `fields`, a `files` block, a column, the `image`, or where an act making its papers into it (`into`) stands, a section naming the act: the made paper reads under the act, one per template. |\n| `app.tasks` | *Noted, never refused:* A task entity an app lists whose rows no write of the app moves to a closed option (its ring stands disabled), or that an act ticks done and none moves back. |\n| `app.declines` | `declines` names decided rules that fire on the app, each with why the app stays as it is: by id, and `records.picture:<entity>` and `model.template-act:<template>`, said once per entity and per template, with what they fire on. |\n\n<!-- generated:end rules-app -->\n\n`writes: "children"` is a desk that adds and edits a record\'s child rows and never\nthe record itself. `reads: "shared"` lifts every read scope the app\'s tables state \u2014\nrefused where none of them states one.\n\n`scope` names a required one-row link of the entity: the app works inside ONE row of\nthe linked entity at a time \u2014 a project, a branch, a season. With no folder remembered\nit opens on the list of them, each with how many of the app\'s rows it holds; inside,\nevery read, count and reading narrows to that one, an add files its row under it (the\nlink is never asked), and the record states its folder in the header. A scope narrows\nthe view; who may read what stays the entity\'s `read_scope`. The folder link is never a\ncolumn, a filter, a search field, a fact or an add\'s question.\n\nAn app whose job reads its rows otherwise than the entity\'s `records` restates them in\nits own `row` \u2014 the cashier\'s figure is what is still owed, its line the service and the\nday. The subtitle, figure and image it states replace the entity\'s wherever that app\ndraws a row of it; every other app reads `records`, and the title is the entity\'s.\n\n### Workflows\n\nA job no press of the screen starts \u2014 an agent or a service handing the app a file, or a list of\nrows to reconcile a table against \u2014 is a workflow the app declares in `workflows`, by alias. The\nmodel states what it takes and what it hands back; a table is named by its entity and a select by\n`<entity>.<field>`, bound to this workspace\'s own at apply. The screen draws nothing for one: it is\ncalled through the app, by `run_app_workflow` or the app\'s API. On an app shared by a public link it\nruns only for someone who holds the app \u2014 a member it is shared with, or a key that reaches it \u2014 and a\nvisitor\'s call is refused as an alias the app does not have.\n\n```jsonc\n"workflows": {\n "reconcile_fees": {\n "description": "Reconciles a visit\'s fees against the lines of the carrier\'s statement.",\n "inputs": {\n "visit": { "type": "record_link", "entity": "visit" },\n "statement": { "type": "file", "required": false },\n "lines": { "type": "array", "items": { "type": "object", "fields": {\n "service": { "type": "select", "field": "fee.service" },\n "amount": { "type": "number" }\n } } }\n },\n "outputs": { "added": { "type": "number" }, "changed": { "type": "number" } }\n }\n}\n```\n\nIts body is set on the app, never stated in the model: `set_app_workflow` with the app, the alias\nand the `source`, leaving `inputs` and `outputs` out \u2014 the body is checked against what the model\ndeclares. Every apply keeps that body and holds what it takes, what it hands back and its\ndescription to the model, refused where the body no longer fits them. Until a body is set the\nworkflow refuses every call, and each apply says so. A workflow the model stops declaring is\narchived with its alias, as one bound on the app by hand that the model never declared is.\n\n<!-- generated:start apps-workflows -->\n\n#### Workflow\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `description` | text | yes | What the workflow does, read by whoever calls it by its alias |\n| `inputs` | map of text \u2192 [Workflow value](#workflow-value) | no | What a call hands it, by name \u2014 checked before its body runs |\n| `outputs` | map of text \u2192 [Workflow value](#workflow-value) | no | What its body returns as `data`, by name \u2014 checked as it returns; absent, read off the body |\n\n#### Workflow value\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"boolean"` \\| `"date"` \\| `"datetime"` \\| `"email"` \\| `"json"` \\| `"record_link"` \\| `"select"` \\| `"member"` \\| `"file"` \\| `"object"` \\| `"array"` | yes | What the value is |\n| `description` | text | no | What the value is for, read by whoever calls the workflow |\n| `required` | `false` | no | false: the value may be left out; absent, it is always there |\n| `multi` | `true` | no | A `record_link`, `select`, `member` or `file` holding several |\n| `entity` | alias | no | The entity a `record_link` names a row of |\n| `field` | text | no | `<entity>.<field>`: the select field whose options a `select` takes, as the field holds them when the workflow runs |\n| `options` | list of alias (at least one) | no | The values a `select` takes, where no field holds them |\n| `fields` | map of text \u2192 [Workflow value](#workflow-value) | no | An `object`\'s values, by name |\n| `items` | [Workflow value](#workflow-value) | no | What each item of an `array` is |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.workflow-value` | A value a declared workflow (`workflows`) takes or hands back states what its type needs and nothing another type does: a `record_link` its `entity`, a `select` its `options` or the `field` holding them \u2014 never both \u2014 an `object` its `fields`, an `array` its `items`; `multi` stands on a `record_link`, `select`, `member` or `file`; an output is never a `member` or a `file`; and a value nests at most 8 deep. |\n\n<!-- generated:end apps-workflows -->\n\n### Declines\n\nA rule the check marks *Refused until adopted or declined* is a treatment the apply requires unless the app\ndeclines it. The apply refuses an app missing one and hands it a patch stating it: merge the patch, or state in the app\'s `declines`\nwhy the app stays as it is, under the key the finding names \u2014 the rule\'s id, or `records.picture:<entity>` and\n`model.template-act:<template>` with what they fire on. A decline naming a rule that is not decided, or one no\nrows of the model could fire, is refused. Only the apply\'s check reads `declines`; the app never does. Which\ntreatment each rule stands for: the `design` reference.\n\n```jsonc\n"declines": { "register.span": "The yard plans by the day each hire is due back, never by its run of days" }\n```\n\n## Register\n\n```jsonc\n"register": { "columns": ["service", "release"], "filters": ["customer", "service"], "create": ["container_no", "customer"] }\n```\n\n<!-- generated:start apps-register -->\n\n#### Register\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `columns` | list of alias | no | Fields read as columns after the row\'s title \u2014 on a calendar\'s entry or a lane\'s block, its facts after its title; never what the row draws itself (its subtitle, image, status, figure, or the bound its figure\'s meter reads against), which the runtime places |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The fields the reader narrows the rows by every day, in that order \u2014 each earns its place by that daily use, so none to 3 is normal and 3 is a cap, never a quota (a select, a member, a link, a date, a yes/no, a number as a range, or its `tiers` where the business narrows by those bands daily) \u2014 each on a desk\'s line; a phone\'s one Filters sheet holds those on a field its rows show there (their name, their line, the one reading beside the name). Never the status (its chips); each a field the rows show \u2014 a column, or a part of the row. A threshold the business names ("large orders") is a formula yes/no here; a column\'s header sorts by a day or an amount |\n| `book` | `true` | no | With `books`: a column naming the table each row is of, and a filter by it |\n| `search` | list of alias (at least one) | no | The fields the search box matches (a link by its row\'s title); a pasted list searches each line and names the lines no row matched. Absent, every word of the row |\n| `group` | alias | no | The field the rows open grouped by, the reader\'s grouping starting there \u2014 a period\'s rows read under each period, one per person; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order rows open in: one key, or a list of keys most significant first, each breaking the ties of the ones before it (a select orders by its options\' order); absent, the first deadline soonest first, else the newest first |\n| `layout` | `"table"` \\| `"cards"` \\| `"calendar"` \\| `"roster"` \\| `"lanes"` \\| `"gantt"` | no | `cards` for rows read by their picture; `calendar` for rows read by their day \u2014 a month, a week, a day or a list, an entry at its hour where its date holds one and over its span where its line holds a second date; `roster` for what each row did on each day of a week or a month (who worked which shift, which vehicle ran), or with `expect` which of a set each row holds; `lanes` for rows booked on a resource over time (an appointment in its chair, a hire on its machine); `gantt` for rows each planned over a run of days, one bar a row on one axis of time (a shipment from its sailing to its arrival, a hire from its start to its return, a task of a project) \u2014 its dates in `gantt`, its lanes the register\'s `group`; absent, a table |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the rows whose select holds one of these options and whose yes/no (stored, or a formula) is this value \u2014 read so on the server and never offered as a chip: the rows this app works of an entity other apps read whole (the purchases, of orders both ways). A row added here holds a select\'s one option and a stored yes/no\'s value |\n| `opens` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The view the register opens on: each entry a narrowing on a field the rows show that stands as a chip the reader may take away (the rows still owed), `"me"` a member field holding the reader (the rows mine). One on the status replaces the chips\' opening on the open work \u2014 `"me"` on a status whose `stages` have owners opens on the rows waiting on the reader |\n| `remove` | `false` | no | The app edits its records and offers no Delete on them (a row\'s \u22EF, its record\'s \u22EF) \u2014 a desk correcting a date of records another app makes and closes; absent, a record the app writes is deleted where it stands |\n| `create` | list of alias (at least one) \\| `false` | no | The fields asked when a row is added; absent, the title, the subtitle and figure no default fills, the natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `readings` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot) \\| [Progress](#progress)) (at least one) | no | Readings of the register\'s own entity above its rows, over the rows in view \u2014 the search, the status and the filters narrow them, but a picture\'s own field, read as the register opens on it. A press on a breakdown\'s part, a pivot\'s cell or a figure with `where` narrows the rows by its field, as a filter does |\n| `attention` | `false` | no | false: a table\'s or cards\' rows keep the register\'s own order (a first-come queue), and no column names whose step each row stands at; absent, those needing attention open first \u2014 those at a step the reader owns, then overdue, then due within 3 days, then those whose line states a warning \u2014 each part in that order, until the reader picks another |\n\n#### Tiered filter\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row |\n| `tiers` | list of number (at least one) | yes | The breakpoints between its bands, ascending, in the field\'s stored unit: under the first, from each to the next, from the last up \u2014 each band worded in the field\'s notation; a filter picks several at once |\n\n#### Sort key\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field rows are ordered by |\n| `desc` | `true` | no | Latest or largest first; absent, soonest or smallest first |\n\n<!-- generated:end apps-register -->\n\n<!-- generated:start rules-register -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.column-drawn` | A column never names what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 and names a field once. |\n| `register.filter` | A filter names a select, a member, a link, a date, a yes/no or a number of the register\'s own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes. |\n| `register.filter-split` | *Noted, never refused:* A filter on a field a breakdown or pivot in the band splits the rows by \u2014 a press on its part narrows by it too; a member\'s filter, which narrows to the reader\'s own rows, is never noted. |\n| `register.filter-unshown` | A filter, a narrowing the register `opens` on, and a press in its band (a breakdown\'s part, a pivot\'s cell, a figure\'s `where`) name a field its rows show \u2014 a column; the row\'s title, the line under it, its picture, status or figure, a meter\'s bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under. A narrowing to the reader (`"me"`) needs none. |\n| `register.tiers` | `tiers` band a number read in one unit on every row; a figure read in each row\'s own unit or currency is named bare. |\n| `register.search` | `search` names text, a code, or a link \u2014 matched by its row\'s title. |\n| `register.sort` | Each `sort` key names a field of the register\'s rows that holds an order \u2014 never files \u2014 and no field twice. |\n| `register.group` | `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and a gantt into its lanes; grouping by the title needs a subtitle to lead each row. |\n| `register.readings-own` | A register\'s `readings` read its own entity; another entity\'s stand on a dashboard. |\n| `register.readings-bands` | A register reading that bands a number by `tiers` has that field with the same tiers among the register\'s `filters`: a press on a band narrows the rows by that filter. |\n| `register.readings-restate` | A register\'s metric never sums the row\'s figure or counts the rows without a `where`: the summary line totals the figure and the status chips count the rows. |\n| `register.where-opens` | `where` and `opens` hold the register\'s own selects and yes/nos, and `opens` a member field holding the reader (`"me"`) \u2014 or, on a status whose `stages` have owners, the rows waiting on the reader; a field `where` fixes is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. |\n| `register.band-pivot` | *Noted, never refused:* A pivot in a register\'s band: its grid reads on a dashboard, and the band leads with a `metric`. |\n| `register.headline` | *Refused until adopted or declined:* A register over rows that move (a status, a deadline) or carry money, laid out as a table, cards or a gantt, states `readings` \u2014 the patch splits the rows by a field they show and reads them over a date. |\n| `register.span` | *Refused until adopted or declined:* Rows that each span a run of days or hours \u2014 two dates on their line, a `frees`, or a `no_overlap` booking them on what its `by` links to \u2014 are drawn in time by some register over them (a calendar, lanes, a gantt or a roster) \u2014 the patch adds an app over them laid out as lanes on what each one books, else as a gantt. |\n| `register.remove` | A register\'s `remove: false` hides the Delete its records would offer, so it stands only where the app writes its records. |\n| `register.attention` | `attention: false` keeps a table\'s or cards\' rows in the register\'s own order \u2014 a first-come queue; a calendar, lanes, a gantt and a roster stand their rows in time and lift none. |\n\n<!-- generated:end rules-register -->\n\nThe status is always the chips, with\ncounts, opening on the rows not closed (every row on a calendar or a lanes board, whose\nwindow of time narrows them) \u2014 never one of `filters`. `filters` are the ones a reader\nnarrows by every day \u2014 none to three, never a quota \u2014 in order of use, each a field the\nrows show (a column, a part of the row), each on the line\nwith the search and the chips, the grouping one\nchip at its end; a member filter offers "mine". A date filters by a range of days \u2014 of\nminutes on a `datetime` field, its end not held, so back-to-back shifts read each row\nonce \u2014 and can keep the rows holding no date too (in the yard at a moment: in before it,\nout after it or not yet). A number filters by a range typed in its\nunit or currency \u2014 a figure read in each row\'s own, in the one picked beside it \u2014 with a\nslider over a percent or a figure against a constant `limits`; `{ "field", "tiers" }`\noffers the bands its breakpoints cut in place of a range (`[1000, 5000]` on a weight in\nkg: under 1.000 kg, 1.000\u20135.000 kg, 5.000 kg and over), refused on a figure read in each\nrow\'s own unit. A threshold with a name is a formula yes/no, never a range. A\ncolumn the row already draws is refused. `sort` is one key or a list of keys, most\nsignificant first, each breaking the ties of the ones before it \u2014 a shipping line in its\nselect\'s option order, then the arrival day soonest first:\n`[{ "field": "shipping_line" }, { "field": "arrived_on" }]`; a header\'s press leads them.\nAbsent `sort`, rows open soonest deadline first where the row has a `due`, else newest first. `search` names the fields\nthe search box matches \u2014 a link by its row\'s title \u2014 and a pasted list searches each\nline, naming the lines no row matched; absent, it matches every word of the row.\n\n`where` keeps the rows the app works, read so on the server and never a chip: a\npurchase desk over orders of both directions states `"where": { "direction":\n["purchase"] }`, and a row added there is written holding the one option (a select kept\nat several is asked among them). `opens` is the view the register opens on, each entry a\nchip the reader may take away: a cashier opens on `"opens": { "owes": true }`. Both take\na select\'s options or a yes/no\'s value, stored or a formula; `opens` on the status is\nthe chips\' opening choice. `opens` also takes `"me"` on a member field \u2014 `"opens": {\n"assignee": "me" }` opens on the rows assigned to whoever reads, the server reading the\nreader, never the page; `where` refuses it, since it fixes the rows for every reader\n(who may read a row at all is the entity\'s `read_scope`).\n\nHow the rows are laid out is \xA7 Layouts, tabs over a period \xA7 Tabs, the report \xA7 Export, and the band above\nthe rows \xA7 Readings.\n\n## Layouts\n\n`layout` is how the register draws its rows \u2014 one of the pages below; absent, a table. Each reads what the\nrows already state (their `records` line, status and dates), and the keys a layout adds stand on its page.\nWhich one a job needs: the `design` reference.\n\n### Table\n\nAbsent `layout`, each row is one line: the entity\'s `records` entry \u2014 its picture or mark, title and the line under it\n\u2014 then the status, the `columns`, and the figure at the right; the title, the status and a column of days or\namounts sort the rows by their header. A table groups its rows by `group` (a gantt draws it as its lanes); a\ntable or cards reads `tabs` and `books`.\n\n### Cards\n\n`layout: "cards"` draws each row as a card led by its picture \u2014 the `records` `image`, or the app\'s\n`row.image`: its title and the line under it, its status and what needs the reader (a deadline, a warning),\nthen its figure and the register\'s `columns` at its foot. Cards are for rows found by their picture (a\nproduct, a vehicle, a damage photo); rows with no picture read better as a table.\n\n### Calendar\n\n`layout: "calendar"` places each row on its day \u2014 a month, a week, a day or a list (a\nphone reads the list or a day): at its hour where the date holds one, over its span\nwhere the row\'s line holds a second date after it, its line\'s other words and the\nregister\'s `columns` after its title. The calendar reads the window in view, and its\ncount and the status chips count that window, opening on every row.\n\n### Lanes\n\n`layout: "lanes"` with `lanes` naming a one-link draws each row of the linked entity as\na lane (a chair, a machine, a room), empty ones too, and each register row as a block\nfrom the first date on its line to the second: over the hours of one day where the first\nholds its hour, else over the days of one day, a week or a month. A block reads the row\'s\nname and the first of its `columns`; a free stretch between blocks adds a row there, its\nlane and its start set, and its end where the next block bounds it. A phone lists each\nlane\'s blocks and free stretches in clock order. Rows naming no lane stand first, in a lane\nof their own; where the app writes the link, each block moves to another lane from its menu.\nInside a folder (`scope`), the lanes are the ones whose own one-link names that folder \u2014 a\nbranch\'s rooms, never every branch\'s. A span\'s end holds its day unless `records` names it\nthe day the row `frees`.\n\n`loads` names what a lane carries: `{ "weight": "payload", "volume": "box" }` sums each\nblock\'s number and reads it against the lane row\'s own, at most two. A lane holds its\nblocks while they stand, so it reads the most they add up to at one time in the window \u2014\na day\'s rows together \u2014 named, past its bound, by how much and on which day or at which\nhour. The two are in one unit, or two of one dimension (kg against t); a block then reads\nwhat it adds, and the move menu reads each lane as it would stand with the block on it.\nA bound a single row must keep (no parcel heavier than its van) is a `write_rules` `max`\nthrough the link, which refuses the move.\n\n<!-- generated:start apps-lanes -->\n\n#### Lanes keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `lanes` | alias | no | With `layout: "lanes"`: a one-link of this entity whose target\'s rows are the lanes (a chair, a machine, a room), each drawn even when empty. A row is a block from the first date on its line to the second \u2014 within one day by the hour where the first holds one, else by the day over one day, a week or a month; a free stretch between blocks adds a row there, its lane, its start and the end the stretch reaches filled |\n| `loads` | map of alias \u2192 alias | no | With `lanes`: what a lane carries against what it holds, at most 2 \u2014 a number of this entity each block adds (a weight) to a number of the lane\'s row it is held within (a payload), in one unit or two of one dimension. Each lane reads the most its blocks add up to at one time in the window \u2014 a day\'s rows together \u2014 and names by how much and when a lane runs over |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.lanes` | `layout: "lanes"` and `lanes` go together: `lanes` is a one-row link to an entity with a `records` entry, the rows name a date on their line, a block\'s two ends are read alike (two days or two moments), and `loads` needs `lanes`. |\n| `register.loads` | `loads` names one to 2 numbers a block adds \u2014 never a percent \u2014 each against a number of the lane\'s row, in one unit or two of one dimension. |\n\n<!-- generated:end apps-lanes -->\n\n### Gantt\n\n`layout: "gantt"` with `gantt` draws each register row as one bar on one axis of time:\n`{ "start": "etd", "end": "eta", "milestones": ["cut_off"], "planned": { "end": "booked_eta" },\n"progress": "done", "after": "waits_on" }`. `end` defaults to the entity\'s first `due`, and an\nentity with neither is refused; `start` and `end` are two dates of the row, both days or both\nmoments, and each planned date is read as `start` is. `progress` is a percent (0\u2013100) \u2014 absent, a\nrow whose record lists its tasks fills to how many are done;\n`after` is a link to rows of the same entity, one or several, each ending before the row\nstarts. The bars stand in the lanes of the register\'s `group`. Where the app writes the\nrow and a person writes a bar\'s date, the bar is dragged through the row\'s save, which then\nwrites those two dates though no section places them; a computed date, one an act sets, or\nan app with `writes: "children"` drags nothing.\n\n<!-- generated:start apps-gantt -->\n\n#### Gantt keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `gantt` | [Gantt bar](#gantt-bar) | no | With `layout: "gantt"`: the dates and facts each row\'s bar is drawn from |\n\n#### Gantt bar\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of this entity each row\'s bar starts at (a sailing, a hire\'s start, a task\'s start) |\n| `end` | alias | no | The date each row\'s bar ends at, read as `start` is \u2014 two days or two moments; absent, the entity\'s first `due` |\n| `milestones` | list of alias (at least one) | no | Dates of the row marked on its bar\'s line as diamonds, each named by its label (a cut-off, a delivery, the end of free time) \u2014 never `start` or `end` |\n| `planned` | [Gantt plan](#gantt-plan) | no | The plan each row is read against, drawn as a thin bar under its own \u2014 each date read as `start` is; a missing one is the bar\'s own |\n| `progress` | alias | no | A percent of this entity (0\u2013100) filling each row\'s bar as far as the work is done; absent, how many of the row\'s tasks are done fills it where its record lists them, else the bar wears its status\'s tone |\n| `after` | alias | no | A link of this entity to its own rows each row waits on \u2014 it starts once they end (finish-to-start), an arrow from each; a row starting before one ends is drawn in the danger tone |\n\n#### Gantt plan\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The date the row was planned to start at |\n| `end` | alias | no | The date the row was planned to end at (a booked arrival) |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.gantt` | `layout: "gantt"` and `gantt` go together: `start` and `end` (absent, the entity\'s first `due`) are two different dates of the row, both days or both moments; each milestone another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity. |\n\n<!-- generated:end apps-gantt -->\n\n### Roster\n\n`layout: "roster"` draws the register\'s rows down the side and the days of a week or a\nmonth across; `roster` names the entity whose rows fill the days \u2014 each stands on one\nregister row by its single link to it, and on the first date of its line (a shift on a\nperson\'s day, a run on a vehicle\'s), on every day through the second date where its line\nholds one. A cell reads its row\'s status, else the first single select on its line (a\nshift\'s kind), several rows their count; a week\'s cells say their words and one more\nfact of the line, a month\'s keep their marks. A day holding none stays empty: nothing\nwas due, which is never an absence. A filled cell opens its row; an empty one adds a row\nthere, the register row and the day set, asking what any add of it asks \u2014 unless no\napp writes those rows (`writes: false`). With `expect` (a single select of those\nrows) the columns are its options in place of days \u2014 a checklist of papers \u2014 and `of`\n(this entity\'s multi-select) names the ones each row expects: a gap among them reads as\none and adds its row, the others stand blank, and each row reads what it holds of them,\n`2/3`. Over days, a roster reads the `cadence` its entity\'s `records` states for its\nrows (a site\'s diary page, Monday to Saturday from breaking ground to handover): a past\nday owed with none is marked missing in the danger tone and counted, today\'s is still\ndue and offered to add, each row reads the days it holds of those owed, and the days\nthe cadence lists none on are quiet. A roster states no `columns` and needs no\n`readings`: its legend counts each state in view.\n\n```jsonc\n"register": { "layout": "roster", "roster": "run", "filters": ["operator"] }\n"register": { "layout": "roster", "roster": "diary_page" }\n```\n\n<!-- generated:start apps-roster -->\n\n#### Roster keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `roster` | alias | no | With `layout: "roster"`: the entity whose rows fill the days \u2014 one single link to this entity, and a date on its row line (`records` title or subtitle). A cell reads its row\'s status, else the first single select on its line, several rows a mark each, the cell read by the one latest in that select\'s options; a row whose line holds a second date after the first fills every day to it; a day with none stays empty, never an absence \u2014 unless this entity\'s `records` `cadence` owes one of them there |\n| `expect` | alias | no | With `roster`: a single select of the roster\'s rows whose options are the columns in place of days \u2014 each cell the row holding that option, read by its status, an empty one added there (a checklist of papers per case); the roster then has no period |\n| `of` | alias | no | With `expect`: this entity\'s own multi-select holding the options each row expects \u2014 its other columns stand blank on that row, never added. Its options are among `expect`\'s |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.roster` | `layout: "roster"` and `roster` go together: the roster\'s entity has one single link to this one and a date on its line, a roster draws no `columns`, `expect` needs `roster` and `of` needs `expect`. |\n| `register.roster-expect` | A roster\'s `expect` is a single select of its rows, and `of` this entity\'s multi-select whose every option is one of `expect`\'s. |\n\n<!-- generated:end apps-roster -->\n\n## Tabs\n\nA register reporting a period \u2014 what stands at its end beside what came and went during it (the stock, the\nhires out, the tickets open) \u2014 reads its rows as `tabs` over one `period` the reader picks above them: an\n`open` tab holds the rows open at the period\'s end, an `in` tab the rows dated within it, each with its count\nand each a sheet of the report. Never date chips set by hand, and never a tab per status, which the chips split.\n\n```jsonc\n"register": { "period": "month", "tabs": [\n { "tab": "in_yard", "label": "In the yard", "open": { "from": "arrived_on", "to": "left_on" } },\n { "tab": "arrived", "label": "Arrived", "in": "arrived_on" },\n { "tab": "left", "label": "Left", "in": "left_on" }\n] }\n```\n\n<!-- generated:start apps-tabs -->\n\n#### Tabs keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tabs` | list of [Register tab](#register-tab) (2\u20135) | no | The lists the register reads its rows as, each a tab with its count over one period the reader picks above them \u2014 the stock at its end beside the arrivals and departures during it, a report\'s sheets; each keeps the register\'s filters and search. Absent, one list. Not with a calendar, roster or lanes, which read their own window |\n| `period` | `"today"` \\| `"week"` \\| `"month"` \\| `"quarter"` \\| `"year"` \\| [Period days](#period-days) | no | With `tabs`: the period they open on \u2014 `today`, `week`, `month`, `quarter` or `year` so far, or `{ days }`, the last that many days through now (a night shift running past midnight reads two); absent, this month so far |\n\n#### Register tab\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tab` | alias | yes | The tab\'s name: its address, and the key its rows stand under in a report |\n| `label` | text | yes | What the tab holds, in the reader\'s words |\n| `in` | alias | no | A date (stored, or a formula): the tab holds the rows dated within the register\'s period, from its start through its last day, or up to its last minute \u2014 the arrivals of a period |\n| `open` | [Tab open](#tab-open) | no | The tab holds the rows open at the period\'s end: begun before it, and not ended by then \u2014 what is in stock, out on hire or still owed at that moment |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order this tab\'s rows read in, as the register\'s `sort`; absent, the register\'s |\n\n#### Tab open\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The date a row begins on (stored, or a formula) |\n| `to` | alias | yes | The date a row ends on (stored, or a formula), empty while it is open |\n\n#### Period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | integer | yes | How many days through today, at most 366 |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.tabs` | A register\'s `tabs` each name a tab of their own, never a key a report already holds; a tab\'s `in` and `open` dates are dates of its rows, `open`\'s two different; its `where` and `sort` hold as the register\'s do, and never narrow by the status, which its chips split. Only a table or cards reads tabs, and a band reading\'s `tab` names one of them. |\n\n<!-- generated:end apps-tabs -->\n\n## Export\n\n`export` saves the register as its report \u2014 the title, a line per chip, the search and\nthe moment it was exported, the readings, and every row in view, at most 20,000 of them.\n`export: true` saves it as a workbook, the readings on a sheet before the rows. A list\nstates the reports the business sends \u2014 one a button, several one menu \u2014 each a `label`,\na `description`, and a `template` filled with `title`, `lines`, `readings`, `at` (the\nmoment it was made), `dates` (each date filter\'s `from` and `to` by field, a day or a\nmoment as the filter bounds the rows) and the rows under the entity\'s alias (an html one\nmade a PDF, an `excel` one a workbook; absent, the workbook). A report\'s `filename` names\nits file in the template grammar over the same keys but the rows \u2014\n`"Stock {{dates.arrived.to | format:\\"dd.MM.yyyy\\"}}"`; absent, the template\'s name, or\nthe label. A report of some rows (`where`), or of one value the reader picks (`per`: one\nline\'s file), first narrows the register as its chips, so the screen shows what the file holds.\n\n<!-- generated:start apps-export -->\n\n#### Export keys\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `export` | `true` \\| list of [Register report](#register-report) (at least one) | no | The rows in view saved as the register\'s report \u2014 its title, a line per thing narrowing the rows, the readings and every row: `true`, one workbook of the register\'s columns; a list, the reports a reader picks from one export menu |\n\n#### Register report\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | yes | The report\'s name, in the reader\'s words \u2014 its entry in the export menu and its file\'s title |\n| `description` | text | no | What the report holds, under its name in the menu |\n| `template` | alias | no | A template filled with the report \u2014 `title`, `lines`, `readings`, `at` (when it was made), `dates` (each date filter\'s `from` and `to`, by field), `period` (the tabs\' `from` and `to`), `per` (the value picked), every row in view (at most 20,000) under the entity\'s alias, and each tab\'s under its name: an html one made a PDF, an excel one a workbook; absent, the workbook of the register\'s columns, a sheet per tab |\n| `filename` | text | no | The file\'s name, in the template grammar over one value each \u2014 `title`, `at`, `dates.<field>.from` or `.to` of a date filter, `period.from` or `.to` with tabs, `per` with a `per`, `lines \\| lookup:<n>` \u2014 `Stock {{per}} {{period.to \\| format:"dd.MM.yyyy"}}`; absent, the template\'s name, or the report\'s title |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The rows the report is of, set on the register as the chips they are before its file is made \u2014 the screen shows what the file holds |\n| `per` | alias | no | A select, a member or a one-link the reader picks one value of from the menu \u2014 the rows narrowed to it, as its chip, and one file of that value |\n| `books` | list of alias (at least one) | no | The rows of these books only \u2014 each the app\'s `entity` or one of its `books`; absent, every book\'s |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.export-report` | A register\'s reports each have their own label; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes. |\n| `register.export-template` | A template an export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the register entity\'s alias \u2014 an entity named none of those \u2014 and is the document of no act. |\n\n<!-- generated:end apps-export -->\n\n## Readings\n\nA reading is one number, or one picture of numbers, over rows. Where it stands decides\nwhich rows: a dashboard reads every row, a register\'s `readings` the rows in view, a\nrecord\'s reading block the child rows under the record. The picture is the runtime\'s,\nchosen from what the reading means \u2014 no key picks a chart.\n\n```jsonc\n{ "metric": "visit", "value": "fees", "where": { "paid": false }, "over": "arrived_on", "label": "Unpaid" }\n{ "metric": "payment", "value": "days_to_pay", "average": true, "over": "paid_on", "better": "down", "label": "Average days to pay" }\n{ "metric": "payment", "value": "late_share", "average": true, "label": "Share paid late" }\n{ "breakdown": "visit", "by": "line" }\n{ "trend": "visit", "over": "arrived_on", "value": "fees" }\n{ "pivot": "visit", "rows": "size", "columns": "line" }\n{ "breakdown": "invoice", "by": { "field": "days_late", "tiers": [1, 31, 61] }, "value": "owed" }\n{ "pivot": "invoice", "rows": "supplier", "columns": { "field": "days_late", "tiers": [1, 31, 61] }, "value": "owed" }\n{ "list": "visit", "where": { "stage": ["in_yard"] }, "columns": ["line"], "app": "gate", "label": "In the yard" }\n{ "list": "task", "where": { "assignee": "me" }, "label": "My tasks" }\n{ "progress": "work_item", "planned": { "start": "plan_start", "end": "plan_end" }, "done": "complete",\n "finished": "accepted_on", "measured": { "rows": "logged_qty", "on": "day", "share": "share_of_item" },\n "weight": "value", "label": "Progress against plan" }\n```\n\nA reading\'s `where` keeps rows by a select\'s options, a yes/no\'s value, or `"me"` on a\nmember field \u2014 the rows that name whoever reads, read so on the server.\n\nA metric averaging its `value` (`average: true`) and a split by a number\'s `tiers` are\n`model/averages-and-bands`.\n\n`progress` reads one parent\'s work against its plan \u2014 a register\'s band over a folder, or a record\'s\nblock \u2014 never a dashboard\'s every row: the share of the work planned and the share done, cumulative day by\nday, today\'s two at its foot. Each row is a part of the work, its plan spread evenly over its planned days\nand weighing its `weight` (its value) or, absent, its planned days. Its share done today is `done`, a\npercent the server computes; on an earlier day, its `done` from its `finished` day on, before it what its\n`measured` rows add up to by then \u2014 each row\'s own share of it, a percent the server computes.\n\n<!-- generated:start apps-readings -->\n\n#### Metric\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `average` | `true` | no | The `value` is averaged, not summed: its sum over the rows holding one, divided by how many hold one \u2014 a row holding none is no part of it. Under a period each period reads its own average, and its change compares two averages; where no row holds one it reads no figure, never 0, and no change. A share is an average too: a number formula that is 100 on the rows that count and 0 on the others, in percent notation, reads as the share of rows that count |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n#### Breakdown\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias \\| [Tiered filter](#tiered-filter) | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under \u2014 or `{ "field", "tiers" }`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n\n#### Trend\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n\n#### Pivot\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias \\| [Tiered filter](#tiered-filter) | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side \u2014 or `{ "field", "tiers" }`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart |\n| `columns` | alias \\| [Tiered filter](#tiered-filter) | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top \u2014 or `{ "field", "tiers" }`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n\n#### Progress\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `progress` | alias | yes | The entity whose rows are the parts of the work, read as the share of it planned and the share done, day by day |\n| `planned` | [Progress planned](#progress-planned) | yes | The days each row\'s work is planned over, its share planned spread evenly across them \u2014 a row missing either is no part of the plan, and neither line counts it |\n| `done` | alias | yes | A number of the row the server computes: its share done today, in percent (0\u2013100) |\n| `finished` | alias | no | A date of the row: the day its work was finished, from which it stands at its `done` |\n| `measured` | [Progress measured](#progress-measured) | no | Before a row is finished, its share done on a day is what these add up to by then, never past its `done`; absent, a row stands at nothing until it is finished, and at its `done` today |\n| `weight` | alias | no | A number of the row: what it weighs in the whole \u2014 its value, its share of the contract; a row holding none weighs nothing. Absent, each row weighs its planned days |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n#### Progress planned\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of the row: the first day of its planned work |\n| `end` | alias | yes | A date of the row: the last day of its planned work |\n\n#### Progress measured\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows record the work as it is done, each through its one-row link to a row of the reading |\n| `on` | alias | yes | A date of those rows (their own, or a lookup): the day each records its work |\n| `share` | alias | yes | A number of those rows the server computes: the share of its row each adds, in percent |\n\n#### List\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `list` | alias | yes | The entity whose rows are listed, each in its `records` anatomy |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `columns` | list of alias | no | Fields read after each row\'s title |\n| `app` | alias | no | An app of this model over the same entity that a row opens in |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n<!-- generated:end apps-readings -->\n\n<!-- generated:start rules-reading -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.value` | A reading sums a number, never a percent \u2014 percents do not add up; a metric\'s `average` reads one. |\n| `reading.average` | `average` averages a metric\'s `value` over the rows holding one, read against a plain `target` \u2014 never a value `limits` bounds, whose bound is a sum. |\n| `reading.split` | `by`, `rows` and `columns` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 or by the bands `tiers` cut a number read in one unit on every row; a pivot crosses two different fields. |\n| `reading.over` | `over` is a date: the row\'s own, stored or a formula, or its parent\'s through a lookup over a one-row link. |\n| `reading.where` | A reading\'s `where` keeps rows by a select\'s options, a yes/no\'s `true` or `false`, or a member field holding the reader (`"me"`). |\n| `reading.target` | `target` reads a value nothing bounds \u2014 a value `limits` bounds reads against the sum of its bound. |\n| `reading.better` | `better` needs `over`: a change is read over the periods its date places the rows in. |\n| `reading.list-app` | A list\'s `app` is a register app of this model over the list\'s own entity. |\n| `reading.progress` | The work against its plan reads dates of its rows for `planned` and `finished`, a number for `done` and `weight`, and rows `measured` through a one-row link to them, by a date of theirs and a number of theirs. |\n\n<!-- generated:end rules-reading -->\n\nA reading counts its rows, or sums the number `value` names \u2014 never a percent. `by`,\n`rows` and `columns` split the rows by one value each: a single select, a one-row link,\none member, or a yes/no \u2014 stored or a formula, its parts read as its label and the\nothers. `over` is a date, the row\'s own (stored or a formula) or its parent\'s through a\nlookup over a one-link: a trend\'s periods, a metric\'s recent periods beside its number,\nand what a dashboard\'s period windows. `ahead: true` reads a trend forward from the\ncurrent period over the rows still to come. `where` keeps rows whose select holds one\nof the options, or whose yes/no is the value, every entry ANDed \u2014 a band leads with the\njob\'s exception as a formula\'s yes/no (what is late, what is short), never a stage the\nstatus chips already count. A metric summing a value `limits` bounds reads against the\nsum of its bound (`10 / 14`); `target` reads one against a number nothing bounds. A\nmetric\'s change is green where it moves the good way: a sum\'s rise, a count kept by\n`where` falling; `better` states the other way where that reads wrong \u2014 money going out\n(`"better": "down"`). A register reads only its own entity; a `list` stands only on a\ndashboard.\n\n`readings` read the register\'s own rows above them: the rows in view, so the search,\nthe status and the filters narrow each one, but a picture\'s own field, which it reads as\nthe register opens on it. A table, cards or a gantt over rows that move or carry money states\nthem (`register.headline`); elsewhere they stand where a picture answers something the rows\ncannot. They stand under the title in one compact\nband: lead with the job\'s headline number (a `metric`), then at most the mix (a\n`breakdown`) and the movement (a `trend`) \u2014 a `pivot` stands under the band as a whole table, every line and its totals, and a\n`breakdown` by a number\'s `tiers` of three breakpoints or more under it too, every band a bar. A picture\nstands only where it splits the rows in view into two parts or more, one of them\nholding two rows or more; a split whose rows\nall fall in one part, and a lone figure with no picture beside it, are said on the\nsummary line instead, so a band stands only holding a picture or two figures. The summary line already totals the\nfigure over the rows in view and the status chips count them, so a `metric` summing the\nfigure, or counting the rows of an entity with a status, is refused unless its `where`\nnarrows it.\n\n## Averages and bands\n\nWhat a [reading](#readings) averages, and how it splits its rows by a number.\n\nA metric\'s `value` is summed; with `average: true` it is averaged \u2014 its sum over the rows the reading\nkeeps, divided by how many of them hold a value, so a row holding none is no part of it. It reads in the\nvalue field\'s own notation, and a figure each row reads in its own unit averages per unit as it sums per\nunit. Under a dashboard\'s period it is the average over the period so far, its shape each recent period\'s\nown average, and its change this period\'s average against the one before\'s \u2014 never a sum of averages.\nWhere no row holds a value there is nothing to average: it reads a dash, never 0, with no change against\nthe period before, and a period holding no row is no point of its shape. A\nplain `target` is the number it is read against; a value `limits` bounds is refused, since its bound is a\nsum.\n\nA share is an average too, so it takes no key of its own: a number formula that is `100` on the rows\nthat count and `0` on the others (`if({paid_on} > {due_on}, 100, 0)`), in percent notation, averaged, is\nthe share of rows that count \u2014 "share paid late". A percent is averaged, never summed. Only a `metric`\naverages: a `breakdown`, a `trend` and a `pivot` count their rows or sum `value`.\n\nA breakdown\'s `by` and a pivot\'s `rows` and `columns` each take, in place of a field, a number by its\n`tiers` \u2014 `{ "field", "tiers" }`, the shape a register\'s [tiered filter](#tiered-filter) takes \u2014 and\nsplit the rows by the bands those breakpoints cut: under the first, from each up to the next, from the\nlast up, a figure on a breakpoint in the band above it. `[1, 31, 61]` over days late reads not due,\n1\u201330, 31\u201360 and over 60 \u2014 the aged report, parties down the side and the bands across. Every band is\ndrawn in the bands\' own order, one holding no row at zero, never ranked by size; each is worded in the\nfield\'s own notation, as the filter\'s chip words it, and rows holding no number stand apart. A band of a\nfigure whose definition makes every value whole \u2014 a rollup that counts, a formula that is\n`differenceInCalendarDays(later, earlier)` \u2014 cut at whole breakpoints reads to the last one it holds, so\nno figure stands in two bands\' words; every other figure, a stored number of days or of a counted noun\ntoo, reads between its breakpoints, since it may hold 14.5. The number is stored, a formula or a rollup, or a lookup of one through\none-row links, read in one unit on every row. On a register a band is a door: a press\nnarrows the rows to it through the filter of the same field and tiers, which the register\'s `filters`\nstate too \u2014 the rows holding no number are no band, so no door; on a dashboard and in a record\'s block\nit opens nothing. A register\'s band seats four bars:\na split by one or two breakpoints stands in it, and one by three or more stands whole under it, as a\npivot does, so every band keeps its bar and its name.\n\n## Dashboards\n\nA dashboard is `{ "alias", "name", "dashboard": [readings] }` with no entity, register\nor record: each reading reads every row of its entity, and one period the reader picks\nat the title \u2014 this month, last month, this or last quarter or year, or any days \u2014 windows\neach reading placed in time: one still running so far, one that closed whole, its change\nread against the period before it. A reading placed in no time reads the rows as they\nstand now, and its head says so. `period` is the one it opens on; absent, the month. A\ntrend holding few periods so far is led in by the ones just before it, so no period opens\non one bar.\nA `list` reading lists rows, each opening in its `app`. The readings answer the one\nquestion the dashboard\'s owner asks, in the order they ask it \u2014 rows to act on lead where\nthe answer is rows; no order of kinds is the rule.\n\n```jsonc\n// The depot owner\'s morning question: which boxes are past their free days, whose are they, is it growing?\n{ "alias": "overview", "name": "Depot overview", "dashboard": [\n { "list": "visit", "where": { "overdue": true }, "columns": ["line"], "app": "gate", "label": "Past free days" },\n { "breakdown": "visit", "by": "line", "where": { "overdue": true }, "label": "Past free days by line" },\n { "trend": "visit", "over": "arrived_on", "where": { "overdue": true }, "label": "Past free days, by arrival" }\n] }\n```\n\n<!-- generated:start apps-dashboard -->\n\n#### Dashboard app\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `dashboard` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot) \\| [List](#list)) (at least one) | yes | The readings, in order \u2014 each over every row of its entity, windowed by the one period the reader switches |\n| `period` | `"month"` \\| `"quarter"` \\| `"year"` \\| [Dashboard period days](#dashboard-period-days) | no | The period the switch opens on \u2014 `month`, `quarter` or `year` so far, or `{ days: 30 }`, the last 30 days through today; absent, this month so far. Needs a reading placed in time by `over` |\n| `workflows` | map of alias \u2192 [Workflow](#workflow) | no | Workflows of the app no act runs, by alias \u2014 called through the app (`run_app_workflow`, its API) by an agent or a service: each states what it takes and hands back, and its body is set on the app (`set_app_workflow`) and kept by every apply. The screen draws nothing for one |\n\n#### Dashboard period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | `30` | yes | The last 30 days through today \u2014 the one span the switch holds |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.period` | A dashboard\'s `period` is the one its switch opens on, and the switch stands only over a reading placed in time by `over`. |\n\n<!-- generated:end apps-dashboard -->\n\n## Record page\n\n```jsonc\n"record": {\n "door": "page",\n "sections": [\n { "title": "Details", "fields": ["size", "line", "built_on"] },\n { "title": "Gate in", "at": ["arriving", "in_yard"], "fields": ["arrived_on", "truck"],\n "blocks": [{ "rows": "fee", "where": { "leg": ["in"] }, "expect": "kind", "columns": ["amount", "paid_by"] }],\n "acts": ["gate_in_paper"] },\n { "title": "Sell to a buyer", "description": "Moves the container to the buyer\'s stock once it has left.", "acts": ["sell"] }\n ]\n}\n```\n\n<!-- generated:start apps-record -->\n\n#### Record page\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `door` | `"page"` \\| `"drawer"` \\| `"beside"` | no | A page for work read at length; a drawer for rows worked one after another from the register; `beside` for rows worked one after another while the list stays in view \u2014 the register\'s list and the open record side by side, over a table register alone. Each stacks its sections, and from three sections on navigates them \u2014 a page by a rail, a drawer by tabs; absent, a page where a section has `at` or is a `page`, the record has more than three sections or one stands beside the rest (`side`), else a drawer |\n| `sections` | list of [Section](#section) (at least one) | no | The record in the order its work reaches it \u2014 each section its fields, then its blocks, then its acts. Absent, one section of every field a person writes that no other place shows |\n| `comments` | `true` | no | People comment on the record: its comments \u2014 who wrote what and when, with a composer \u2014 stand in its Activity, opened from its head; stated only where the job\'s people discuss a record or the owner asks for one. Absent, no one comments |\n| `history` | `true` | no | Draws nothing of its own: the record\'s Activity shows every move of the status as a change, when and who \u2014 refused beside `activity: false`, which shows the moves nowhere; noted beside Activity, and refused there in a later release. The entity\'s `status.history` is written either way |\n| `activity` | `false` | no | false: the app\'s records show no activity \u2014 the door in each record\'s head and the panel of its saves and comments beside it; where `comments` is stated, the door opens the comments alone. Absent, every record shows it |\n| `compose` | [piece](#composed-page) | no | The record\'s page stated as a tree of finished pieces instead of `sections`, on a page door \u2014 for a page that is its own arrangement of what the app already states: each node names a piece of the catalog (`use`) and binds by name to a field of the entity, an act of the app or a child entity; it holds no condition and no computed value |\n\n#### Section\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | text | yes | What the section is, in the reader\'s words \u2014 its heading, and its name on the record\'s rail or tab |\n| `description` | text | no | One sentence under the title: what the section is for, or what its act does |\n| `at` | list of alias (at least one) | no | Options of the record\'s status (or its milestones) during which this section is the work now \u2014 a step of the path the record moves through: its rail item reads now while the status holds one and this app has work there, done once the record\'s history says it went through one \u2014 or it is a step of the path before where that history opens (with none, where the record stands) that no act moves the work past \u2014 and it stands before the section that is the work now, and waiting otherwise; a section every option of which ends the work, past the path\'s end or entered by its own act, is an exit: no item and nothing drawn until the record stands in it, then set apart after the steps and never one; A page opened from the register opens at it. It never locks what the section holds: its fields are written as the write rules and checks allow at any stage. An act it names is offered during these alone: its `when` holds the status to them, or on milestones it stamps the step after one of them \u2014 a detour and an exit are sections of their own, a detour stated before the step it returns to |\n| `fields` | list of alias (at least one) | no | The record\'s own fields in this section, in the order they are read \u2014 never one the header draws: the row\'s title, subtitle, image, status or figure |\n| `blocks` | list of [block](#blocks) (at least one) | no | What the section holds besides its own fields, under them, in order |\n| `acts` | list of alias (at least one) | no | Acts of this app drawn under the section\'s fields, their asks as more of them and what each lacks said above its button; the header draws no act, so every act of the record itself is named by one section \u2014 an act of a child (`of`) stands on the child\'s row and is never named here. An act moving the status, offered at ONE step (the stage its `when` holds), stands at that step\'s foot when it moves the work forward \u2014 to a later option, an exit too \u2014 or back to a step the work passes anyway; one offered at several steps, or moving back into a detour, stands in the section it enters, which draws it while the work waits \u2014 an exit\'s in the header\'s \u22EF, since an exit stands only once the record is in it. A correction \u2014 `danger` and not one of its step\'s outcomes (no other status move offered at the same moment) \u2014 waits in the section heading\'s \u22EF, and one no section names in the header\'s \u22EF; every other act is named by a section |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `side` | `true` | no | The section the rest are worked from, drawn in a pane beside them rather than in their scroll \u2014 the photos looked at, the pool picked from, the paper typed off; at most one on a record, which opens on a page door. It holds what any section holds |\n| `page` | `true` | no | A page of its own, off the record\'s scroll: its own heading and a way back, reached from the record\'s rail after the scroll\'s sections, or from a link at its place where the record has no rail \u2014 for what makes the record worse stacked in it: a long list of related rows, a workspace of its own. Only on a page door, never a step (`at`). Holding only one `text` block over a markdown field, it reads as a document: a 680px column, saved as it is written \u2014 whether it is saved said at its foot |\n\n<!-- generated:end apps-record -->\n\n<!-- generated:start rules-record -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.placed-once` | A field, a block and an act each stand once on a record: the header draws the title, subtitle, image, status (or milestones), figure and folder, and a field\'s one other place is a section\'s `fields`, a files, text or statement block \u2014 save the image, which one files block may draw again, whole, the figure, which a statement draws as what it comes to, and rows one list prices and another compares (`compare`); two places whose `when`s never hold together (a select at options the other\'s lacks) each draw it. |\n| `record.foot-total` | A rows block\'s foot totals the child\'s figure and each summed column over the rows it lists; the record\'s rollup summing that field over those rows (the same `where`) is that foot, never also one of a section\'s `fields`. |\n| `record.fact-restates` | A section\'s field never looks up what its link already reads where it stands: the linked row\'s title and line beside the link, the figure or bound its meter reads on the header\'s line. |\n| `record.section-holds` | A section holds fields, blocks or acts. |\n| `record.sections` | *Refused until adopted or declined:* A register laid out as a table, cards or a gantt states its record\'s `sections` \u2014 the patch holds the fields people write, a files block per paper, a block per child and the record\'s acts. |\n| `record.children` | *Refused until adopted or declined:* An entity some app records, whose rows other rows each belong to (a one-row link to it, never its status history), draws some of them under its row \u2014 a block on its record, a roster of them, lanes of them by that link, or an app over them working in one of its rows at a time (`scope`) \u2014 the patch lists each under the record. |\n| `record.at` | `at` names options of the record\'s stored status, or its milestones; a record with no status has no steps. |\n| `record.at-once` | A stage is the work of one section: two sections naming one option of the status, or one milestone, in `at` hold some field to values apart in their `when` \u2014 a `when` holding the status keeps its section off the options it leaves out. |\n| `record.section-page` | A section is a `page` only on a page door \u2014 never a drawer\'s or one beside the list \u2014 and never a step (`at`), its title naming an address no other page shares (a letter or a digit at least), and never every section. |\n| `record.side` | At most one section stands beside the rest (`side`), on a page door, and never also a page of its own. |\n| `record.beside` | A `door: "beside"` record opens beside a table register \u2014 never a board, cards or a calendar, which draw no list to stand beside. |\n| `record.history` | `record.history` names a status history the entity records (`status.history`), on a record whose Activity is on \u2014 its Activity shows each move, so beside `activity: false` the key draws nothing; no block reads that history. |\n| `record.history-activity` | *Noted, never refused:* `record.history: true` beside a record whose Activity is on: Activity shows every status move as a change already, so the key adds nothing \u2014 a later release refuses it. |\n| `record.section-acts` | A section\'s `acts` name acts of the record itself: an act of a child (`of`) stands on the child\'s row, and one over the register\'s rows (`on`) on the register. |\n| `record.act-in-section` | Every act of the record stands in the `acts` of the section whose work it is; only a correction no section owns \u2014 `danger`, and no other status move offered at the same moment \u2014 or an act an agent runs for the person (a `workflow` or an `intake` moving no status), left off every section, waits in the header\'s \u22EF. |\n| `record.move-home` | A status move offered at one step stands at that step\'s foot when it moves the work forward, to an exit, or back to a step it passes anyway; one offered at several steps, or moving back into a detour, stands in the section `at` the status it enters. |\n| `record.act-staged` | An act at a staged section\'s foot is offered only during that section\'s steps: its `when` holds the status to options the section names \u2014 on milestones, it stamps the step after one of them. |\n| `record.act-hidden` | An act is never offered only while its section\'s `when` hides the section. |\n\n<!-- generated:end rules-record -->\n\nThe record is its `sections`, stacked in the order the work reaches them. A section is\nits fields, the acts under them, then its blocks \u2014 the acts at its foot instead where one\nreads what a block holds or none of its fields \u2014 and holds at least one of them. From\nthree sections on, the record is navigated by their titles: a page by a rail that jumps\nto each (a strip of the same names on a phone), a drawer by tabs;\nwith fewer, the sections just stack. `description` is one sentence under the title.\n`at` names the status options (or the milestones) during which the section is the work\nnow \u2014 only on a record that truly moves through those stages: the rail marks it done,\nnow or waiting (an item of no `at` is its title alone), and the section stands even holding\nnothing yet \u2014 it never hides a field, locks it or gates an act, and a record opens at its\nhead. `page: true` makes a section a page of its own under the\nrecord\'s, off its scroll, with a way back \u2014 rarely, for a long related list or a workspace\nof its own that would make the record worse stacked; never on a drawer or a step. The\nheader draws no act: every act on the record is named by the section whose work it is, and one no section names is refused, naming its likely\nsection \u2014 only a correction no section owns waits in the header\'s \u22EF beside Delete. An act\nmoving the status and offered at ONE step (its `when`) stands at that step\'s foot when it\nmoves the work forward \u2014 to the next step, or to an exit later in the status\'s options\n(Reject beside Approve) \u2014 or back to a step the work passes anyway. One offered at SEVERAL\nsteps, or moving back into a detour, is named by the section it enters: a detour draws it and its\ngaps alone while it waits. A detour \u2014 a stage the work only steps aside into from one step and\nleaves only back to it (a pause, a repair) \u2014 stands before that step, its option and its section\nalike; listed after, the work reads it as its next step, and the check notes it with the move\nlisting it before. A `danger` act is a correction \u2014 waiting in the \u22EF of what it acts\non: its section\'s heading, its row, the header\'s where no section names it \u2014 unless it moves\nthe status while another status move is offered at the same moment; then it is one of that\nstep\'s outcomes, a button. Cancel offered at Held and at Confirmed, beside Confirm, is an\noutcome into an exit: state `{ "title": "Cancelled", "at": ["cancelled"], "fields": ["reason"],\n"acts": ["cancel"] }`. An exit stands only once the record is in it, its reason there and its\nrail item apart from the path; until then Cancel waits in the header\'s \u22EF. Every record\nopens its activity from its head \u2014 each save field by field, each move among them, and each\ncomment, with one composer \u2014 unless `activity: false`. `comments: true` lets people comment\n\u2014 opt-in: state it only where the job\'s people discuss a record; with `activity: false` the\ndoor opens the comments alone, and with neither no door stands. `history: true` draws\nnothing of its own \u2014 each move is already in the activity: noted, refused beside\n`activity: false` (the moves would show nowhere), and refused everywhere in a later release.\n\nA field, a block and an act is each placed ONCE on a record \u2014 the header, one section;\na second place is refused. The header draws the entity\'s `records` title, subtitle,\nimage, status (or milestones) and figure, so none of them is named again in a section\'s\n`fields` or the register\'s `columns` \u2014 save the image, which one `files` block may draw\nagain, whole at reading size (an incident\'s photo, a scan), the header keeping its mark. Refused too: a section holding nothing, an `at`\non a record with no status, and a section\'s act that is a child\'s or runs over the\nregister\'s rows.\nA table, cards or a gantt states its record\'s `sections` (`record.sections`); absent them \u2014 a register laid\nout in time, or one declining the rule \u2014 one section holds every field a person writes that the header does\nnot show. An editable field rests as its control and saves as it changes; everything\nelse is plain text. A section or a block with `when` is shown only while each named\nselect of the record holds one of its options. The one layout serves every entity: a\ncustomer or a product is sections without `at`.\n\n## Blocks\n\n```jsonc\n"blocks": [\n { "rows": "fee", "columns": ["kind", "amount"] },\n { "rows": "fee", "where": { "leg": ["out"] }, "columns": ["kind", "amount"], "under": "invoice" },\n { "rows": "cost", "via": "bill", "create": false, "remove": false, "pick": true },\n { "files": ["photos", "papers"] },\n { "text": "remarks", "when": { "stage": ["held"] } },\n { "statement": [{ "field": "subtotal" }, { "field": "vat", "op": "plus" }, { "field": "total", "op": "equals" }] }\n]\n```\n\n<!-- generated:start apps-blocks -->\n\n#### Rows block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | A child entity whose rows belong to this record |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s fields read as columns after its title \u2014 never what its row draws (subtitle, image, status, figure, or the bound its figure\'s meter reads against), though an `expect` block\'s figure is a column, filled in place. |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the child rows whose single select holds one of these options, or whose yes/no (stored, or a formula) is this value \u2014 every entry ANDed, read so on the server; a row added here holds the selects\' (one leg\'s fees, of a record holding both legs\'). Each select is one the child requires, so no row stands in no block |\n| `expect` | alias | no | The child\'s title when the record holds one row per value of it \u2014 a single select (one per option, in order) or a one-row link to a catalog (one per row the link may point at): a value not yet filled reads as its own empty line, and no value holds two rows. The block\'s `columns` name at least one field a person fills in place, unless it states `post`, whose lines are ticked |\n| `of` | alias | no | With `expect` on a select: the record\'s own multi-select holding the options this record expects a line for \u2014 the options it holds, in order, rather than every option. Its options are among the title\'s |\n| `under` | alias | no | The child\'s one-link to a document that covers its rows (an invoice over its fee lines), whose many-link points back and which links to this record: each document stands as a heading over the lines it covers; a filled line no document covers says so on its own row with a make for one document over it, and two or more waiting take a combined make in the block\'s heading. Never also a rows block of the document |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The child\'s fields the reader narrows the block\'s rows by, as a register\'s `filters` \u2014 none to 3, each a field its rows show (a column, or a part of the row), its status among them where the business narrows by it \u2014 never on a task list, whose status chips narrow it |\n| `search` | list of alias (at least one) | no | The child\'s fields the block\'s search box matches (a link by its row\'s title), a pasted list line by line; absent, the block has no search box |\n| `group` | alias | no | The child\'s field the block\'s rows open grouped by, as a register\'s `group` \u2014 the reader\'s grouping starting there; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order the block\'s rows open in, as a register\'s `sort` \u2014 within each group where the block groups them; absent, the child\'s first deadline soonest first, else the newest first |\n| `edits` | list of alias (1\u20132) | no | The child\'s fields each line holds as its control, written where the line stands (a quote line\'s sell price) \u2014 a number, a date, words or one option the row\'s save writes, each a column or the figure, at most 2; every other value reads, and the row opens to write it |\n| `remove` | `false` | no | This list offers no Delete on its rows (its open row and \u22EF); they are deleted where another list of them or their own record offers it \u2014 e.g. lines that are a selection of rows kept elsewhere |\n| `pick` | `true` | no | The block also picks rows of the child that no record holds through `via` yet \u2014 those naming what this record names in each link `via`\'s `same` compares, and holding the block\'s `where` \u2014 and links them here; a row picked leaves the block from its \u22EF, unlinked and kept. `via` is an optional one-row link a person writes, and the record holds the many-link paired with it |\n| `checklist` | [Checklist](#checklist) | no | The child\'s rows, which are tasks, read as a checklist of checks: each one\'s mark \u2014 passed at a `closed` option, failed at a `failed` one, else still to check \u2014 its title, its columns as its words (a text column its note, on its own line), and on a line under its title its photo and its clause; never grouped nor written in place |\n| `compare` | [Compare](#compare) | no | This list reads each row\'s before against its after (a repair\'s damage and its repair, a check failed and redone): once the before holds a photo, the two stand side by side at one size, each row read in full as a card \u2014 on a checklist, whose `photo` is the before, the after beside it on its line \u2014 and an after not taken yet is the camera\'s tile adding it while the row\'s work is open; a list of the same rows without it stays a table. Never with `expect` |\n| `post` | [Post](#post) | no | This list is a publish desk: with `expect` on a one-row link to a catalog of destinations, every destination the link admits is a line a person ticks \u2014 a tick makes its row from its key alone, an untick deletes it until its status is `closed` \u2014 and the line chosen reads beside the list as the post that destination\'s feed will show, with its words to copy, the block\'s `columns` as its facts and the row\'s acts. Never with `under`, `pick`, `checklist`, `compare` or a `create` list |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Checklist\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `photo` | alias | no | The child\'s files field holding the photo each row is checked by |\n| `clause` | alias | no | The child\'s one-row link to what each row is checked against \u2014 a standard\'s clause, a rule |\n| `failed` | list of alias (at least one) | no | Options of the child\'s status a row stands failed at \u2014 none of them `closed` |\n\n#### Compare\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `before` | alias | yes | A files field of the block\'s rows: the photo of the place before the work (the damage surveyed, the check failed) |\n| `after` | alias | yes | Another files field of the block\'s rows: the photo of the same place once the work is done (the repair, the redo) |\n\n#### Files block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `files` | list of alias (at least one) | yes | Files fields of this record, read as pictures and documents |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Text block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `text` | alias | yes | A long text field read as prose |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n<!-- generated:end apps-blocks -->\n\n<!-- generated:start rules-block -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.child-link` | A block reads a child linked to the record; `via` names the link where the child links more than once. |\n| `block.column-drawn` | A child block\'s column never names what the child\'s row draws \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 nor its link back to the record, and names a field once; an `expect` block\'s figure is its filled-in-place column. |\n| `block.title-back` | Where a child\'s title is its link back to the record a block lists it under, its first other subtitle names it there \u2014 a text, a code, a select, a link or a member, never a number or a date. |\n| `block.where` | A rows block\'s `where` names single selects the child requires, by options they hold \u2014 one held at a single option is never also a column \u2014 and the child\'s yes/nos by value. A yes/no is never held: a block adds rows only at a stored one\'s start, and never picks, expects or stands lines under documents. |\n| `block.under` | `under` names the child\'s one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines\' columns never read the document (its heading does) nor hold long text. |\n| `block.under-once` | Documents drawn over their lines (`under`) are never listed again as a block of their own. |\n| `block.edits` | A rows block\'s `edits` name at most two of the child\'s columns or its figure that the row\'s own save writes \u2014 a number, a date, a few words or one option \u2014 never on an `expect` block, whose lines are filled in place already. |\n| `block.remove` | A rows block\'s `remove: false` hides the Delete its rows would offer, so it stands only where the app writes those rows. |\n| `block.compare` | A rows block\'s `compare` names two different files fields of the child: its `before` and its `after` \u2014 never on a block with `expect`, and on a `checklist` its `before` is the check\'s `photo`. |\n| `block.checklist` | A rows block\'s `checklist` reads a task entity\'s rows as checks, one under another \u2014 never expected, under documents, grouped, written in place or picked together by an act over several, its columns no files; its `photo` names a files field of the child, its `clause` a one-row link of the child, and its `failed` options of the child\'s status none of which is `closed`. |\n| `block.pick` | A rows block\'s `pick` links rows of the child through `via` \u2014 an optional one-row link a person writes, no act writes, no other rule compares rows by and no `under` block stands lines under \u2014 paired with a many-link of the record its own save writes; the child\'s `read_scope` reads a row naming no record through `via`; never on an `expect` or `under` block, nor in an app writing only the record\'s children. |\n| `block.narrow` | A rows block\'s `filters`, `search`, `group` and `sort` follow the register\'s rules over the child\'s rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, never on an `expect` block, and an `under` block is grouped by its documents. |\n| `block.expect` | `expect` names the child\'s title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the block; the block\'s `columns` name at least one field a person fills in place \u2014 unless it states `post`, whose lines are ticked \u2014 and its `create` takes the file where the child is pictured by one. |\n| `block.expect-fills` | An expected line is made from its key and its first filled column \u2014 a `post` block\'s from its key alone, by its tick \u2014 so every other field the child requires has a default, a start or a `default_from`, or is read first. |\n| `block.expect-of` | A block\'s `of` needs `expect` on a select, and names the record\'s multi-select whose every option is one of the title\'s. |\n| `block.field-kind` | A files block names files fields of the record, and a text block a text field. |\n| `block.read-scope` | A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history\'s, which takes its record\'s. |\n| `block.under-note` | *Noted, never refused:* Rows holding a paper that cover another block\'s lines, listed apart with no add of their own stated: `under` draws them over the lines they cover. |\n| `block.expect-note` | *Noted, never refused:* Rows each a kind of paper \u2014 a single select their title, a files field holding it \u2014 listed with no `expect` and no `where` on the kind, that no act of the app files a call into or `fills`: the papers a record needs read as the lines still missing. |\n\n<!-- generated:end rules-block -->\n\nAn add in a block fills the link to this record, never asking it, and a rows block opens each row in a drawer.\nUnder `under` the uncovered lines come first; a document made over\none line starts its figure at the line\'s amount, the heading\'s make covering those ticked in its dialog. `pick` (a\nsupplier bill picking the job\'s cost lines) offers none until the record names each link `via`\'s `same` compares,\nlinks at most 200 ticked, and saves the record: a check locking it refuses, and one refusing a row\'s edits refuses\nthat row. Where the child is pictured by a file a person brings (its `records` `image`), an `expect` line not yet\nfilled is a placeholder naming the paper whose upload makes the row; a catalog link\'s values are the rows its\n`options_where` admits, and the add offers only values not yet held. A field starting from the catalog row\n(`default_from`) shows that value as its hint, taken with one press. A block narrowed by a select\'s `where` expects\nits lines once per value among them (one leg\'s fees beside the other\'s), its foot totalled by the record\'s sum\nnarrowed alike. An `agenda` and a `timeline` are set out under \xA7 Timeline and agenda, a `statement` under\n\xA7 Statements.\n\n## Composed page\n\nA record whose page is its own arrangement of what the app already states \u2014 a piece of writing under its title, a\nbrief beside its figures \u2014 states `compose` instead of `sections`: a tree of finished pieces, each named by `use`.\n\n```jsonc\n"record": { "door": "page", "compose": {\n "use": "Column", "children": [\n { "use": "Bar", "children": [\n { "use": "Status" },\n { "use": "Field", "field": "audience" },\n { "use": "Act", "act": "send_for_review" },\n { "use": "Copy", "field": "body", "as": "plain" } ] },\n { "use": "Title" },\n { "use": "Document", "field": "body" },\n { "use": "Group", "title": "Reviews", "children": [\n { "use": "Rows", "child": "review", "create": false },\n { "use": "Act", "act": "ask_reviewer" } ] } ] } }\n```\n\nA node binds by name: a `field` is a field of the record\'s entity, an `act` an act of the app, a `child` an entity\nlinking to the record. What a name binds is read and written exactly as a section naming it would be \u2014 a `Field` and\na `Document` are saved through the record\'s own save, an `Act` runs its workflow, a `Rows` reads the child\'s rows \u2014\nso a tree adds no write of its own. It holds no condition and no computed value: a piece that does not apply on a\nrow (a field its entity\'s `applies` leaves out, an act its `when` does not offer, rows with nothing in them and no\nadd) draws nothing, by the rule that leaves it out of a section, and anything that must depend on the row is an act\'s\n`when` or a field\'s formula, never the tree.\n\nThe first `Act` of the tree is the page\'s primary and every other one secondary; a `danger` act reads so wherever it\nstands. A `Copy` and a `Document` naming one field copy what is on screen \u2014 the words typed and not yet saved among\nthem. The record\'s chrome stays the runtime\'s: the way back, its Activity and its \u22EF with Delete share the row over\nthe tree, and the head draws no title and no status of its own \u2014 the tree\'s `Title` and `Status` do.\n\n<!-- generated:start apps-compose -->\n\n#### Column piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Column"` | yes | The page\'s reading column, 680px: its pieces stacked in order from one left edge |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces stacked in it, in the order they are read |\n\n#### Bar piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Bar"` | yes | One row of small things \u2014 a status, a field\'s control, an act, a copy \u2014 which wraps where it runs out of room |\n| `children` | list of [piece](#composed-page) (at least one) | yes | Its pieces, in order; a `Field` directly in it is its control alone on one line, named by its label |\n\n#### Group piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Group"` | yes | A titled stack, headed once by `title` |\n| `title` | text | yes | Its heading, in the reader\'s words; holding one `Rows`, it carries their count and their add, and the group\'s `Act`s end its line |\n| `children` | list of [piece](#composed-page) (at least one) | yes | The pieces under the heading, in order |\n\n#### Title piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Title"` | yes | The record\'s title (`records.<entity>.title`) as the page\'s heading, written in place where a person writes it; one a page |\n\n#### Status piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Status"` | yes | The record\'s status (`records.<entity>.status`) as a record\'s head draws it: moved by hand, by its acts\' moves, or read |\n\n#### Field piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Field"` | yes | A field of the record under its label, as a section\'s `fields` draws it |\n| `field` | alias | yes | The field drawn \u2014 never the title or the status, which `Title` and `Status` draw; once a page |\n\n#### Document piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Document"` | yes | A markdown text field written in place with no box, saved as it is written \u2014 whether it is saved said at its foot |\n| `field` | alias | yes | A text field with `format: "markdown"`; once a page |\n\n#### Act piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Act"` | yes | An act of the app on the record as its button: its `when`, what it lacks, its asks and its confirmation as everywhere; the first `Act` of the tree is the page\'s primary |\n| `act` | alias | yes | An act of this app on the record \u2014 never one `of` a child\'s rows, which stands on its row; once a page |\n\n#### Rows piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Rows"` | yes | A child entity\'s rows under the record, as a rows block lists them, each row\'s own acts on it |\n| `child` | alias | yes | A child entity linking to the record; once a page |\n| `create` | `false` | no | false, rows are not added here \u2014 an act of the page makes them |\n\n#### Copy piece\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `use` | `"Copy"` | yes | A button copying a field\'s words as the page holds them now \u2014 what is typed in its `Document` and not yet saved among them |\n| `field` | alias | yes | The text field copied |\n| `as` | `"plain"` \\| `"raw"` | yes | `plain`, its markdown marks gone; `raw`, as stored |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.compose` | A record\'s `compose` is stated instead of `sections`, on a page door, by an app of one book whose records have no steps (`status.milestones`, `status.stages`) and no picture (`image`) \u2014 a composed page draws neither. Each name is a field of the entity, an act of the app on the record or a child entity linked to it; one `Title`, and each field, act and child drawn once \u2014 a field by `Field` or `Document`, never the title or the status, which `Title` and `Status` draw. The first `Act` of the tree is the page\'s next step and every other one secondary; an `Act` is a button alone, never one making a set of papers. A `Field` directly in a `Bar` is its control alone on one line, named by its label; a `Bar` holds no `Rows` and no `Document`. A `Group` is headed once, by its `title`: holding one `Rows`, that heading carries the rows\' count and their add in place of the rows\' own, and the `Act`s the group holds end its line. A `Copy` takes a text field\'s words as the page holds them now, a `Document`\'s words not yet saved among them; a field a `Copy` alone names is read, never written. |\n\n<!-- generated:end apps-compose -->\n\n## Publish desk\n\nRows that are each a place the record is sent \u2014 a notice\'s boards, a listing\'s\nportals \u2014 are a rows block that `expect`s the child\'s one-row link to the catalog of those places and states `post`:\n\n```jsonc\n{ "rows": "posting", "expect": "board", "columns": ["posted_link", "refusal"],\n "post": { "bodies": ["wording", "wording_abroad"], "text": "goes_out_as", "media": "pictures", "link": "page",\n "group": "reach", "network": "kind", "networks": { "company_page": "linkedin" } } }\n```\n\nThe desk is where the post is written. `bodies` are the record\'s plain text fields it is written in (one per\nlanguage, say), each its editor between the list and the preview, saved as any field of the record is and drawn\nnowhere else on it. The preview is `text` read over what is on screen: where `text` is a formula of the child, the\ndesk computes that same formula over what is typed \u2014 a body before it is saved, the line\'s own column \u2014 so the post\nmoves as it is written, and a place not ticked previews the words its row would hold. For that the formula reads only\nwhat a screen holds whole: stored values of its row, the record through lookups over the link back to it, the place\nthrough lookups over `expect`, and formulas of the row over those \u2014 a rollup, a lookup through another link or the\nclock is refused, naming the field.\n\nEvery place the link admits is a line with a tick and that place\'s face: its picture (its `records` `image`), else\nthe mark of the network it is on where `networks` names one, else none. `group`, another single select of the place,\nstands the lines under its options in their order: a kind holding a ticked line is open, one holding none folded to\nits heading and its count \u2014 the first open where nothing is ticked \u2014 and a place of no kind comes last. A tick makes\nthe line\'s row from its key alone \u2014 so nothing else the child requires is left without a default \u2014 and an untick\ndeletes it until the row\'s status is `closed`: what went out is not taken back. It asks first only where a person\nwrote on the row \u2014 its status moved off its first option, or one of the block\'s `columns` a person writes filled. One line is worked beside the list \u2014 the one\npressed, else the first ticked \u2014 and read as the post that place\'s feed shows: `text` is the child\'s words for that\nplace, `media` and `link` the record\'s, and `network` the place\'s single select whose options `networks` names to a\nfeed. A place on an option not named reads its words on a plain card. Under the bodies stand the block\'s `columns` a\nperson writes of the line\'s row (words for that place alone); under the post, the press copying its words beside\nthe acts `of` the child \u2014 never a second primary: the section\'s own act is that \u2014 then what the row\'s line and the\nblock\'s `columns` hold that nobody writes there (the link it went out at, why it was refused), each drawn only once\nit holds a value. A value an act or a workflow fills is stated as that act\'s, so the desk never offers it as an\nempty box. Where the block\'s box seats three columns they are the list, what is written and\nthe post; narrower, what is written stands over the post; narrower still the list stands alone and a line opens in\nplace. Sending is an\nordinary act of the record, named in the section\'s `acts`.\n\n<!-- generated:start apps-post -->\n\n#### Post\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `bodies` | list of alias (1\u20133) | no | The record\'s plain text fields the post is written in, in order \u2014 one per language, say \u2014 at most 3: each its editor on the desk, between the list and the preview, and drawn nowhere else on the record |\n| `text` | alias | yes | A text of the block\'s rows \u2014 stored, or a formula yielding text \u2014 holding the words that go out on that row\'s destination. A formula is computed on the desk too, over what is typed: it reads stored values of its row, the record through lookups over the link back to it, the destination through lookups over `expect`, and formulas of the row over those \u2014 never a rollup, another link or the clock |\n| `media` | alias | yes | A files field of the record: the pictures or the video the post carries |\n| `link` | alias | no | A text field of the record holding a link (`format: "link"`), carried as the post\'s card |\n| `network` | alias | yes | A single select of the entity `expect` links to, saying which network each destination is on |\n| `group` | alias | no | Another single select of the entity `expect` links to \u2014 the destination\'s kind \u2014 the lines stand grouped by: each option a heading in the select\'s order, a group holding no ticked line folded to its heading and its count, destinations holding no option last |\n| `networks` | map of alias \u2192 `"facebook"` \\| `"instagram"` \\| `"threads"` \\| `"x"` \\| `"linkedin"` | yes | That select\'s options, each by its alias, to the network whose feed the post is previewed in \u2014 facebook, instagram, threads, x, linkedin. An option not named has no feed to imitate: its words read as a plain card |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.post` | A rows block\'s `post` needs `expect` on a one-row link \u2014 never a select, nor with `under`, `pick`, `checklist`, `compare` or a `create` list, its line made by a tick from its key alone: its `bodies` are plain text fields of the record a person writes, drawn nowhere else on it; its `text` is a text of the child, stored or a formula yielding text \u2014 a formula reading only stored values of its row, lookups through the link back to the record or through `expect`, and formulas of the row over those, never the clock; its `media` a files field of the record and its `link` a text field of the record; its `network` a single select of the entity the link names, and `networks` holds options of that select, by alias, each to one of facebook, instagram, threads, x, linkedin; its `group` is another single select of that entity. |\n\n<!-- generated:end apps-post -->\n\n## Timeline and agenda\n\nTwo blocks read a child\'s rows by their day: a `timeline` logs what happened, newest first; an `agenda` plans\nwhat is to come on a time axis.\n\n```jsonc\n"blocks": [\n { "timeline": "note" },\n { "agenda": "booking", "columns": ["guide"], "start": "arrival" }\n]\n```\n\n<!-- generated:start apps-timeline-agenda -->\n\n#### Timeline block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `timeline` | alias | yes | A child entity read as a log of dated entries, newest first, with a composer where the app adds entries, each corrected and removed where it stands as a rows block\'s row (entries planned ahead are an `agenda`) \u2014 never the record\'s status history: its Activity shows each move |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, the subtitle and figure no default fills, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Agenda block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `agenda` | alias | yes | A child entity read as planned entries on a time axis: by day, soonest first, each entry with its picture and the day that is today marked. The day is the first date on the child\'s row line (its `records` title or subtitle); within a day entries follow a datetime there, else the first single select after the date \u2014 its options in the day\'s order |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s facts read on an entry\'s line after its title \u2014 never what its line already draws (subtitle, image, status, figure) |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `start` | alias | no | The record\'s date that is the first day: each day\'s heading counts from it (Day 1, Day 2); absent, the weekday and the date alone |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.timeline` | A timeline\'s child holds a date to place its entries by. |\n| `block.agenda` | An agenda\'s child names a date on its row line (`records` title or subtitle); `start` is a date of the record. |\n| `block.timeline-note` | *Noted, never refused:* Rows each what happened on a day and who \u2014 a date on their line, a member, no status and no figure \u2014 listed as a table: a `timeline` draws them as a log, newest first. |\n| `block.agenda-note` | *Noted, never refused:* Rows each on a day and within it (at an hour, in a part of the day) listed as a table: an `agenda` draws them by day. |\n\n<!-- generated:end apps-timeline-agenda -->\n\nAn entry with no day yet is a plan, listed first and dated in place; a timeline\'s moment is its child\'s first date,\nand who its member field.\n\n## Statements\n\nA `statement` block draws a figure the record works out from its own figures \u2014 a claim\'s payable, an invoice\'s\ntotal, a payslip\'s net \u2014 line by line in the order the arithmetic runs: each line a number of the record, the first\nstating no `op`, the last an `equals`. Every figure is a field the server computes or a person writes; the screen adds\nnothing up. A line\'s `parts` lists the rows its figure carries, each read from its own row, and that figure is what\nthey add up to \u2014 a formula, rollup or lookup, or the server\'s copy when the record is made (`default_from`), which\nnothing writes after. A claim\'s "certified before" is the contract\'s certified total copied when the claim is made\n(`made_at`, a moment the record `starts` at `today`), over the claims certified (`certified_at`, an act\'s `now`)\nbefore that moment, each at the figure its certifying act froze (`certified_value`, set to\n`{ "formula": "{claim_value}" }`, which the contract\'s total sums) \u2014 so they always add up to it, as a signed\ncertificate does, after a percent is edited or the contract varied.\n\n```jsonc\n{ "statement": [\n { "field": "contract_value" },\n { "field": "done_to_date", "op": "times" },\n { "field": "value_to_date", "op": "equals" },\n { "field": "certified_before", "op": "minus",\n "parts": { "rows": "claim", "same": "contract", "at": "certified_at", "before": "made_at", "where": { "kind": ["work"] },\n "columns": ["claimed_on", "done_to_date"], "figure": "certified_value" } },\n { "field": "claim_value", "op": "equals" },\n { "field": "retention", "op": "minus", "rate": "retention_rate" },\n { "field": "payable", "op": "equals" }\n ],\n "when": { "kind": ["work"] } }\n```\n\n<!-- generated:start apps-statement -->\n\n#### Statement block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `statement` | list of [Statement line](#statement-line) (at least 3) | yes | A figure built from figures of the record, read as a statement: each line its operator, its label and its figure, the figures on one right edge, in the order stated \u2014 the last an `equals`, what it all comes to, drawn heaviest (a claim\'s payable, an invoice\'s total, a payslip\'s net). A line\'s figure is a control where the app writes it |\n| `title` | text | no | The block\'s heading; absent, the label of what the statement comes to |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Statement line\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number of the record \u2014 stored, a formula, a rollup or a lookup \u2014 read on the line after its label |\n| `op` | `"plus"` \\| `"minus"` \\| `"times"` \\| `"equals"` | no | How the line works on the lines above it \u2014 `plus`, `minus`, `times`, or `equals` for what they come to, a formula, rollup or lookup the server computes; absent on the first line, which the rest work from |\n| `rate` | alias | no | A percent of the record read after the line\'s label: the rate its figure is taken at (retention at 5 %) |\n| `parts` | [Statement parts](#statement-parts) | no | The rows the line\'s figure carries, each listed under it with its words and its own figure \u2014 read from those rows, never a copy of them. The line\'s figure is what they add up to, drawn read: a formula, rollup or lookup, or a value the server copies when the record is made (`default_from`) that nothing writes after |\n\n#### Statement parts\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | The entity whose rows the line\'s figure carries \u2014 the record\'s own for the earlier rows of the same thing |\n| `same` | alias | yes | A one-link the record and those rows both hold: the rows naming what the record names in it |\n| `at` | alias | no | A moment (a datetime) of those rows no person keys \u2014 when each was certified: with `before`, only the rows whose moment stands before the record\'s, earliest first; absent with it, every row so named, in their own order |\n| `before` | alias | no | A moment (a datetime) of the record no person keys \u2014 when it was made: the moment the rows\' `at` stands before; stated with `at` |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the rows whose single select holds one of these options, or whose yes/no (stored, or a formula) is this value \u2014 read so on the server |\n| `columns` | list of alias (1\u20132) | yes | The rows\' fields read as each one\'s words, in order (its date, its percent) |\n| `figure` | alias | yes | The rows\' number each one reads at the figures\' edge, frozen when the row was certified: a stored number an act fills (`{ "formula": \u2026 }` as it runs) or the server copies when the row is made (`default_from`) \u2014 never one a person keys, nor a formula, rollup or lookup the server recomputes |\n\n<!-- generated:end apps-statement -->\n\n<!-- generated:start rules-statement -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `statement.lines` | A statement\'s lines are numbers of the record, each once \u2014 the first stating no `op`, every other one, the last an `equals` \u2014 each `equals` a formula, rollup or lookup, and a `rate` a number of the record. |\n| `statement.rows` | A line\'s `parts` read rows of an entity holding `same`, a one-link of the record\'s to the same entity; `at`, a moment of those rows, and `before`, a moment of the record (datetimes, never days), stated together; their `columns` and `figure` are those rows\' fields, the figure a number. |\n| `statement.parts-keyed` | A line\'s `parts` add up to its figure, so no person keys it: a formula, rollup or lookup, a value the server copies when the record is made (`default_from`) that no add asks and no save or act writes, or a stored number stating `writes: false` \u2014 the line draws it read; each part\'s `figure` is frozen when its row was certified \u2014 a stored number an act fills, the server copies or the model states no person keys (`writes: false`), never one a person keys or the server recomputes \u2014 and no person keys the moments `at` and `before` either. |\n\n<!-- generated:end rules-statement -->\n\n## Reading blocks\n\nA `metric`, `breakdown`, `trend`, `pivot` or `progress` block on a record is a reading (\xA7 Readings) of a child\nentity\'s rows under it, read only \u2014 `via` names the link where the child has more than one.\n\nA `figures` block reads the record\'s own numbers instead, each a tile under its field\'s label: two to eight\nnumbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number\nstating `writes: false` (what a workflow or an integration writes). A number a person keys is a field of the\nsection, where it is written. A tile holding nothing reads as not measured, never 0 \u2014 a sum or a count of\nnothing yet reads 0; a stored 0 reads 0.\n\nMetric blocks and a `figures` block standing one straight after another in a section draw as one strip: four\ntiles to a line on a desk, two on a phone.\n\nA `funnel` block reads the same rows as steps people drop out of between: each of its `steps` a number column\nof the child, summed over the rows under the record, drawn in order with the share each keeps of the one before.\nFunnel blocks standing one straight after another in a section draw on one grid \u2014 one name column, one bar origin\nand one value column \u2014 each under its own `label`, its bars over its own first step.\n\n```jsonc\n"blocks": [\n { "metric": "fee", "value": "amount", "label": "Fees" },\n { "breakdown": "fee", "by": "kind" },\n { "trend": "fee", "over": "charged_on", "value": "amount" },\n { "pivot": "fee", "rows": "kind", "columns": "method" },\n { "progress": "task", "planned": { "start": "plan_start", "end": "plan_end" }, "done": "complete", "label": "Progress" },\n { "funnel": "channel_week", "steps": ["impressions", "clicks", "bookings"], "label": "Reach to booking" },\n { "figures": ["sessions", "bookings", "calls", "ad_spend"] }\n]\n```\n\n<!-- generated:start apps-reading-blocks -->\n\n#### Metric block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `average` | `true` | no | The `value` is averaged, not summed: its sum over the rows holding one, divided by how many hold one \u2014 a row holding none is no part of it. Under a period each period reads its own average, and its change compares two averages; where no row holds one it reads no figure, never 0, and no change. A share is an average too: a number formula that is 100 on the rows that count and 0 on the others, in percent notation, reads as the share of rows that count |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Breakdown block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias \\| [Tiered filter](#tiered-filter) | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under \u2014 or `{ "field", "tiers" }`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Trend block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Pivot block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias \\| [Tiered filter](#tiered-filter) | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side \u2014 or `{ "field", "tiers" }`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart |\n| `columns` | alias \\| [Tiered filter](#tiered-filter) | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top \u2014 or `{ "field", "tiers" }`, a number in the bands its tiers cut (days late: not due, 1\u201330, 31\u201360, over 60), every band drawn in its own order and a row holding no number apart |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Progress block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `progress` | alias | yes | The entity whose rows are the parts of the work, read as the share of it planned and the share done, day by day |\n| `planned` | [Progress planned](#progress-planned) | yes | The days each row\'s work is planned over, its share planned spread evenly across them \u2014 a row missing either is no part of the plan, and neither line counts it |\n| `done` | alias | yes | A number of the row the server computes: its share done today, in percent (0\u2013100) |\n| `finished` | alias | no | A date of the row: the day its work was finished, from which it stands at its `done` |\n| `measured` | [Progress measured](#progress-measured) | no | Before a row is finished, its share done on a day is what these add up to by then, never past its `done`; absent, a row stands at nothing until it is finished, and at its `done` today |\n| `weight` | alias | no | A number of the row: what it weighs in the whole \u2014 its value, its share of the contract; a row holding none weighs nothing. Absent, each row weighs its planned days |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Funnel block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `funnel` | alias | yes | A child entity whose rows each count the same steps, a column a step (a week\'s channels, each with its impressions, clicks and bookings): one funnel over the rows under the record \u2014 each step its column summed, its bar over the first step, the share kept between two steps and from the first to the last. A step no row holds a number in reads as not measured, never as zero |\n| `steps` | list of alias (2\u20138) | yes | The child\'s numbers summed, in order, first step to last \u2014 2 to 8, each stored, a formula or a rollup, all in one notation, never a percent |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Figures block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `figures` | list of alias (2\u20138) | yes | Numbers of this record no person keys, in order, each a tile under its field\'s label \u2014 2 to 8: a formula, a rollup, a lookup, or a stored number stating `writes: false`. A tile holding nothing reads as not measured, never 0 \u2014 a sum or a count of nothing yet reads 0 |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.funnel` | A funnel\'s `steps` are 2 to 8 numbers of its child, each named once \u2014 stored, a formula or a rollup \u2014 all in one notation, never a percent nor a unit read on each row. |\n| `block.figures` | A `figures` block\'s tiles are 2 to 8 numbers of the record no person keys \u2014 a formula, a rollup, a lookup through links to one row, or a stored number stating `writes: false` \u2014 each placed once on the record. |\n\n<!-- generated:end apps-reading-blocks -->\n\n## Tasks\n\nAn entity whose `records` states `"task": true` \u2014 its status a stored select with `closed` \u2014 is\nwork someone finishes. Every table of its rows draws it alike \u2014 a rows block of the record it\nbelongs to, its own register, the rows filed under one of its rows (its steps) \u2014 each row led by a\nring. Ticking the ring moves the row to a closed option by the one write the app already has for\nthat move \u2014 the act of that entity setting a closed option (its first preferred), offered on an\nopen row and asking nothing, else its own save where people write the status \u2014 stamping what\nthat act stamps and appending the status history. Unticking runs the act moving a closed row\nback (its `when` holding a closed option), else the save back to the status\'s default. A move no\nwrite reaches leaves the ring disabled, never hidden, and the check notes it. The status is moved\nin place on the row by the same moves; every other field is edited where the row opens. A\nfiles column reads on a task\'s line as how many files it holds. An act `of` the child with\n`on: "rows"` completes several at once: the list\'s heading offers to select rows, and the act\nruns on those picked. A block with `expect` or `under` draws its own lines, never rings. A record listing rows of a\ntask entity filed under it by a link holding one row says how much of that work is done \u2014 those rows at a closed\noption of all, every one the reader may read, every such block\'s rows counted as one \u2014 wherever its row is drawn: a\nmeter on its head, a column of its register\'s rows, a card\'s foot, a calendar entry, a gantt\'s or a lane\'s bar; a\ngantt of task rows grouped by the row they are filed under says that row\'s on each group\'s head. No rollup is stated\nfor it.\n\n```json\n{\n "entities": [\n {\n "alias": "project",\n "label": "Projects",\n "singular": "Project",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "lead", "label": "Lead", "type": "select_member" }\n ]\n },\n {\n "alias": "task",\n "label": "Tasks",\n "singular": "Task",\n "fields": [\n { "alias": "title", "label": "Title", "type": "text", "required": true },\n {\n "alias": "project",\n "label": "Project",\n "type": "select_record_link",\n "target_entity": "project",\n "cardinality": "one",\n "required": true\n },\n { "alias": "assignee", "label": "Assignee", "type": "select_member" },\n {\n "alias": "status",\n "label": "Status",\n "type": "select",\n "required": true,\n "options": [\n { "alias": "todo", "label": "To do", "color": "slate" },\n { "alias": "done", "label": "Done", "color": "green" }\n ],\n "default": ["todo"]\n },\n { "alias": "due", "label": "Due", "type": "date", "format": "date" },\n { "alias": "done_on", "label": "Done on", "type": "date", "format": "date" }\n ]\n }\n ],\n "records": {\n "project": { "title": "name" },\n "task": {\n "title": "title",\n "subtitle": ["assignee"],\n "status": { "field": "status", "closed": ["done"] },\n "due": ["due"],\n "task": true,\n "starts": { "assignee": "me" }\n }\n },\n "apps": [\n {\n "alias": "projects",\n "name": "Projects",\n "entity": "project",\n "record": {\n "sections": [{ "title": "Work", "fields": ["lead"], "blocks": [{ "rows": "task", "columns": ["due"] }] }]\n },\n "acts": [\n { "alias": "complete", "label": "Complete", "of": "task", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "reopen", "label": "Reopen", "of": "task", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } },\n { "alias": "complete_all", "label": "Complete", "of": "task", "on": "rows", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } }\n ]\n },\n {\n "alias": "my_tasks",\n "name": "My tasks",\n "entity": "task",\n "register": {\n "opens": { "assignee": "me" },\n "readings": [{ "metric": "task", "where": { "status": ["todo"] }, "label": "Open" }]\n },\n "record": {\n "door": "drawer",\n "sections": [{ "title": "Task", "fields": ["project", "due"], "acts": ["finish", "undo"] }]\n },\n "acts": [\n { "alias": "finish", "label": "Finish", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "undo", "label": "Undo", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } }\n ]\n }\n ]\n}\n```\n\n## Acts\n\n```jsonc\n"acts": [{\n "alias": "gate_out", "label": "Gate out",\n "when": { "stage": ["in_yard"] }, "requires": ["release", "seal"],\n "asks": ["seal", { "input": "note", "label": "Note", "type": "long_text" }],\n "set": { "stage": "gone", "left_on": "now", "left_by": "me", "remark": "input:note" },\n "confirm": true\n}]\n```\n\n<!-- generated:start apps-acts -->\n\n#### Act\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The act\'s name within its app \u2014 its workflow is `act_<alias>` |\n| `label` | text | yes | The verb, as the reader says it |\n| `on` | `"record"` \\| `"rows"` \\| `"view"` | no | `rows` runs on the rows ticked in the register \u2014 with `of`, on the child rows ticked together in a rows block of the record; `view` on every row the register shows (offered while the view is narrowed to its `when`; it states no `requires`); absent, on one record (its page and its row\'s menu) |\n| `of` | alias | no | A rows block\'s child entity the act works on: offered on each child row \u2014 or, `on: "rows"`, on the rows ticked together in the block \u2014 its conditions and its write that row\'s |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The act works only while each named select holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `requires` | list of alias (at least one) | no | Fields that must be filled first, each drawn where the act stands (its section\'s fields, a block\'s, or the act\'s asks \u2014 never the header alone); a blocked act names each one missing |\n| `recommends` | list of alias (at least one) | no | Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank |\n| `asks` | list of (alias \\| [Asked input](#asked-input)) (at least one) | no | What the reader states when pressing it \u2014 fields of the record (written by the act) or inputs |\n| `set` | map of alias \u2192 (text \\| number \\| boolean \\| `null` \\| [Set formula](#set-formula)) | no | What the act writes: an option alias, a value, `"now"`, `"me"` or `"input:<name>"` \u2014 each the act\'s outcome, read-only elsewhere; or `{ "formula": \u2026 }`, computed over each acted row as it stands when the act runs and written then (a sell price at cost and markup): a starting value, which a person may change after and the act never recomputes |\n| `workflow` | alias | no | An authored workflow (src/workflows/<alias>.ts) the act runs after its checks, for what `set` cannot say |\n| `writes` | list of alias (at least one) | no | With `workflow`, fields of the acted row its body writes (an id kept from a service\'s answer) \u2014 the act\'s, as what `set` states is |\n| `template` | alias | no | A document template the act makes a file from \u2014 an html one made a PDF, an excel one a workbook; over several rows, one file of them all, the rows listed under the entity\'s alias |\n| `templates` | list of [Act template](#act-template) (at least 2) | no | Several papers the act makes into `into` (a case\'s forms), instead of one `template`: listed where the act stands \u2014 each made one as its file, each not yet made as a placeholder \u2014 the reader ticks which to make, one press making those, each replacing its own last paper |\n| `into` | alias | no | The record\'s files field the made document is kept in \u2014 remade, it replaces its own last paper, never a person\'s file |\n| `paper` | text | no | Its paper, `<child>.<kind>`: the line of the record\'s papers it stands on |\n| `intake` | alias | no | A child the record lists in a rows block whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child \u2014 its kind the child\'s title, its file in the child\'s files field |\n| `record` | [Recording](#recording) | no | The press records a call, visit or meeting through the host \u2014 Stop ends it \u2014 and files it as a new row of a child the record lists: its audio, its screen where captured, its transcript |\n| `sign` | [Signature](#signature) | no | The press opens a pad the signer signs on, beside what the act asks, and the act keeps the signature as a picture with who signed, when and the witness \u2014 read on the record as one signature, and handed to the paper the act makes |\n| `fills` | list of text (at least one) | no | With `intake`, what the papers read may fill: a field of the record, `<link>.<field>` on the row a required one-row link names, or a child whose rows the papers add \u2014 shown before and after, saved where it differs; a single link is filled with a row the app reads. With `record`, fields of the new row an agent fills from the transcript, saved as they come |\n| `confirm` | `true` | no | The press asks first; the question lists every active warning |\n| `danger` | `true` | no | The act destroys or cannot be undone |\n\n#### Asked input\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `input` | alias | yes | A value the act asks for that is not a field of the record |\n| `label` | text | yes | What the reader is asked, in their words |\n| `type` | `"text"` \\| `"long_text"` \\| `"number"` \\| `"date"` \\| `"select"` \\| `"link"` \\| `"member"` | yes | What the reader states |\n| `options` | list of [Select option](#select-option) (at least one) | no | The choices a `select` input offers, one picked \u2014 written into a select holding the same option aliases |\n| `entity` | alias | no | The entity a `link` input picks one row of |\n| `required` | `true` | no | The act is refused without it |\n| `default` | alias | no | A field of the row whose value the input starts from, still changed at will |\n\n#### Set formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | text | yes | A formula over the acted row\'s fields, `{alias}` each, read as the entity\'s own formulas read them |\n\n<!-- generated:end apps-acts -->\n\n<!-- generated:start rules-act -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.paper-note` | *Noted, never refused:* A made paper kept in a field of a record that lists its papers by kind: it is one of those lines. |\n| `act.moves` | *Refused until adopted or declined:* Rows a stored status stages towards `closed` options are moved by an act of some app over them, or by its authored `workflow` \u2014 the patch moves each open option to the next, the last to the first closed one. |\n| `act.set-not-asked` | A field an act `set`s \u2014 an option, a value, `now`, `me`, an input \u2014 or its workflow `writes` is the act\'s outcome: read-only everywhere else and never asked by an add (`create`); a required one takes a `default`. A field an act computes by a `formula` is a starting value, which a person may change after. |\n| `act.writes` | `writes` stands beside `workflow` and names stored fields of the acted row, each once, that no `set` of the act and no other act over the rows states. |\n| `act.set-formula` | `set: { <field>: { formula } }` reads fields of the acted row and computes what the field holds \u2014 a number, a date, text or a yes/no, typed as the entity\'s own formulas are \u2014 never the status or a milestone; a bound a write rule holds the field to is checked on the computed value as the act runs. |\n| `act.of` | An act\'s `of` names a child the record lists in a rows block; it works on the one child row it is pressed on \u2014 or, `on: "rows"`, on the rows ticked together where the record lists that child as tasks \u2014 never on the rows in view. |\n| `act.writes-line-key` | An act never writes the link or the key naming an expected line under its record \u2014 a line\'s key is stated by filling it. |\n| `act.makes-or-runs` | An act makes a document (`template`, `templates`) or runs an authored `workflow`, never both \u2014 the workflow makes what it needs. |\n| `act.view` | An act over every row in view (`on: "view"`) writes (`set`), makes a document or runs a workflow, and states no `requires`. |\n| `act.requires-drawn` | Every field an act `requires` is drawn where the act stands \u2014 its section\'s fields, a block\'s, its asks, or an unstaged section\'s where its own is unstaged \u2014 never the header alone. |\n| `act.requires-written` | A field an act requires that only an act writes is written by an act offered together with it, or asked by the act itself. |\n| `act.recommends` | A field an act `recommends` is not also in its `requires`. |\n| `act.set` | `set` writes each field a value it holds \u2014 an option alias, a number, a yes/no, a text, `"now"` on a date, `"me"` on a member, `"input:<name>"` of an input the act asks of the same kind \u2014 or clears an optional one with `null`. |\n| `act.set-milestone` | An act stamps a milestone, never clears one \u2014 unticking clears it and every later one. |\n| `act.paper` | `paper` is a kind the record lists its papers by, on one record \u2014 never beside `into` or `templates`; one maker per kind. |\n| `act.history-copy` | What a status move asks is copied onto the history\'s field of the same name, which holds the same kind of value. |\n| `act.input` | An input has a name of its own \u2014 never another ask\'s, a field\'s, or the record input its workflow takes \u2014 `options` exactly when it is a select, `entity` exactly when it is a link, and a `default` field holding what it states. |\n\n<!-- generated:end rules-act -->\n\nAn act whose `when` does not hold is not offered \u2014 each select named holding one of its\noptions, each yes/no named (stored, or a formula: a period that has ended) its `true` or\n`false`; every one that holds stands at the\nfoot of the section naming it in `acts` \u2014 a correction in that section heading\'s \u22EF, one\nof a child row on its row (a correction in the row\'s \u22EF). An act\'s `requires` are fields\nits section draws \u2014 its `fields`, a block\'s, the act\'s `asks`, or those of another section\nwith no `at` where its own has none; one shown only in the header or a staged section\nelsewhere is refused. One whose `requires` are\nempty is offered disabled, naming each; one whose `recommends` are empty names what\nit will leave blank and still presses; one a standing check blocks is offered\ndisabled in the check\'s words. The generated `act_<alias>` workflow re-checks all three on the\nserver, writes `set` and what was asked, and appends the status history where the\nact moves the status. A field an act asks is read-only everywhere else. A `select` input\noffers its own options and is written into a select holding the same option aliases; a `link` input picks one row of its `entity`\nand a `member` input one person, each written into a field of its kind. A field an act\nboth `requires` and `asks` never disables the press \u2014 the act asks it, required.\n\n`on: "view"` runs on every row the register shows \u2014 its search, status and filters \u2014\neach row checked before any is written. `of` names a rows block\'s child entity: the\nact is offered on each child row (its menu and its drawer), its `when`, `requires`,\n`set` and checks are the child\'s, it runs on that row, and a check of the record that\nlocks the child rows locks it too. With `on: "rows"` it runs on the child rows ticked\ntogether in the record\'s task list (\xA7 Tasks) \u2014 every row checked before any is written;\nit is refused where the record lists no tasks of that child.\n\nWhat an act makes, reads, records or has signed is set out under \xA7 Papers and recordings.\n\n## Papers and recordings\n\nBeside what it writes, an act makes papers from templates (`template`, `templates`), reads papers brought into the\nrecord (`intake`), records a call or a visit (`record`) and takes a signature on the device (`sign`).\n\n<!-- generated:start apps-papers -->\n\n#### Act template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `template` | alias | yes | A document template the act makes a paper of, kept in `into` \u2014 an html one made a PDF, an excel one a workbook |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked |\n\n#### Recording\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | A child the record lists in a timeline or rows block, linking back to it by a single link: each recording is filed as a new row of it |\n| `audio` | alias | yes | The child\'s files field the recording\'s audio is kept in |\n| `transcript` | alias | yes | The child\'s text field the transcript is kept in, a speaker\'s turn per line |\n| `video` | alias | no | The child\'s files field the screen is kept in, where the recording captured it |\n| `at` | alias | no | The child\'s date field stamped with the moment the recording started |\n| `by` | alias | no | The child\'s member field set to the person who recorded |\n\n#### Signature\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | The record\'s files field the signature is kept in, as a picture \u2014 each signing replaces the one before |\n| `by` | alias | yes | Who signed: a text field the signer types their name into (someone with no seat in the workspace \u2014 a driver, the owner\'s supervisor), or a member field set to the person pressing, who signs as themselves |\n| `at` | alias | yes | The record\'s date field stamped with the server\'s time as the act runs |\n| `witness` | alias | no | Beside a text `by`, the member field set to the person pressing, who saw the signer sign |\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.into` | `into` is a files field of the one record the act is pressed on, beside its `template` or `templates`. |\n| `act.templates` | `templates` names each template once, kept in `into`, on one record pressed alone \u2014 never beside `template`, `asks`, `of` or an `on` over several rows. |\n| `act.intake` | `intake` names a child the record lists in a rows block that expects its title \u2014 a single select, the paper\'s kind \u2014 and is narrowed by no `where`, holding exactly one files field and needing nothing an added line does not fill; the act runs on one record, states `fills`, and states nothing but its `label`, `when` and `requires` beside them. |\n| `act.record` | `record` names a child the record draws in a timeline or a rows block with no `expect`, `under` or `where`, linking back by one single link, its row needing nothing the recording does not write: `audio` and `video` files fields, `transcript` text, `at` a date, `by` a member, each its own field; on one record, stating only `label`, `when`, `requires` and `fills` beside it. |\n| `act.fills` | `fills` names each once, only beside `intake` or `record`. Beside `record`: a field of the recording\'s row the recording does not write, never a link. Beside `intake`: a field of the record, `<link>.<field>` on the row a required one-row link names (never beside that link), or a child the record adds rows of in a rows block with no `expect`, `under` or `where`, filled in the fields that block adds \u2014 every one a new row needs among them. Each field filled is one a person writes \u2014 text, a number, a date, a yes/no, options or a single link\'s row \u2014 never a status, a milestone, a field an act writes or a link `same` narrows; a link\'s row is one the app reads of its entity. |\n| `act.fills-drawn` | *Noted, never refused:* A field `fills` writes that the record draws nowhere: the review shows the change, the record does not. |\n| `act.fills-new` | *Noted, never refused:* A child `fills` adds rows of, whose natural key the papers cannot state: every row read is added, so the same papers read twice list them twice. |\n| `act.sign` | `sign` runs on one record, never a child\'s row: `into` a files field, `by` a text field the signer types or a member field set to the person pressing, `at` a date, `witness` a member field \u2014 stated exactly beside a text `by` \u2014 each a field of its own that the act does not also ask, set or `writes`, and no other act signs into `into`. |\n\n<!-- generated:end apps-papers -->\n\n`template` makes a document from the row as the act left it and\nkeeps it in `into`; over several rows it makes one document of them all, the rows\nlisted under the entity\'s alias, and hands it back. `templates` instead names a set of\npapers kept in `into`, listed where the act stands \u2014 each made one as its file, each\nnot made yet as a placeholder \u2014 those whose `when` holds ticked to start; the press\nmakes the ticked ones (sent in `templates`), each replacing the file its template made\nbefore (the file whose `document_template_id` is that template \u2014 never a file a person\nput there), and the set downloads as one archive (`archive_<alias>`).\n\n`intake` reads papers: the press takes files, an agent (`act_<alias>` among the app\'s\nagents) reads them, and each is filed as the record\'s line of its kind in the named\nchild \u2014 that child\'s title \u2014 or a new line, its file in the child\'s one files field.\nWhat they state of each field `fills` names is shown beside what the row holds; the save\nwrites only what differs \u2014 the record\'s fields, `<link>.<field>` on the row a required\none-row link names, a child\'s rows (one found by the child\'s `natural_key` changed, else\nadded) \u2014 each refused as its own save is, and nothing written while one refuses. A single\nlink among them is filled with a row the agent finds through the app\'s read of the rows\nit may name \u2014 narrowed as its picker is \u2014 and an id naming no row of its entity is refused\nbefore the run ends; a link `same` narrows is not filled.\n\n```jsonc\n{ "alias": "read_papers", "label": "Read papers", "intake": "customer_paper", "fills": ["phone", "contact.email", "branch"] }\n```\n\n`sign` takes a signature on the device in front of the signer: the press opens a pad in a\nsheet with what the act asks \u2014 the signer\'s name, where `by` is a text field they type it\ninto \u2014 and the act keeps the picture in `into` in place of any before it, `at` stamped with\nthe server\'s time and the person pressing in `witness` (a member `by` is the person\npressing, signing as themselves). The record reads `into` as one signature \u2014 the stroke,\nwho signed, as what, when and before whom \u2014 and the act\'s paper is handed the picture under\n`into`\'s name, an html template printing it by its url (`{{#each signature}}<img src="{{url}}">{{/each}}`).\n\n```jsonc\n{ "alias": "gate_in", "label": "Make the gate-in EIR", "template": "eir_in", "into": "eir",\n "sign": { "into": "driver_signature", "by": "driver_name", "at": "signed_at", "witness": "signed_before" } }\n```\n\n## Checks\n\n```jsonc\n"checks": [{ "field": "release_warning" }, { "field": "unpaid", "blocks": ["gate_out"] }, { "field": "locked", "blocks": ["edit"] }]\n```\n\n<!-- generated:start apps-checks -->\n\n#### Check\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A yes/no or text formula of the record (or of the child `of` names): true or non-empty means the check stands; its label is its short name on a row\'s line, and a text formula\'s words are the sentence the record and a tooltip say |\n| `of` | alias | no | A rows block\'s child entity this check is a formula of: standing on a child row, it refuses what it blocks of that row |\n| `blocks` | `"all"` \\| list of (alias \\| `"edit"` \\| `"delete"`) (at least one) | no | Acts, "edit" (the saves and the Delete), "delete" (the Delete alone) or "all" that the standing check refuses; absent, it only warns |\n| `resolve` | alias | no | An act of this app, on the rows the check stands on, that clears it: the check\'s line links to the act where it stands, and the act keeps its own conditions |\n\n<!-- generated:end apps-checks -->\n\n<!-- generated:start rules-check -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `check.field` | A check\'s `field` is a yes/no or text formula of the record, or of the child `of` names. |\n| `check.of` | A check\'s `of` names a child the record lists in a rows block. |\n| `check.not-placed` | A check\'s field is never also placed on the record \u2014 the line under the header says it. |\n| `check.blocks` | A check of the record blocks its acts, `"edit"`, `"delete"` or `"all"`; a check of a child blocks that row\'s saves, its Delete, the acts `of` that child, or all \u2014 never the record\'s acts. |\n| `check.resolve` | `resolve` names an act of this app on the rows the check stands on, pressed on one row, not refused by the check, and drawn as a button \u2014 never a correction. |\n| `check.restates-meter` | A warning reading a gated meter\'s figure against its bound or pass mark is refused \u2014 the meter draws it. |\n\n<!-- generated:end rules-check -->\n\nA check is a yes/no or text formula of the record: it stands while true or\nnon-empty, and a text formula\'s words are what the reader sees. Without `blocks`\nit warns; with them it refuses the acts named, `"edit"` the record\'s own saves (and\nits Delete), `"delete"` its Delete alone, or `"all"` \u2014 on the server, in every write it\nnames. A check `of` a rows block\'s child stands on each child row: it refuses that\nrow\'s saves (`edit`), its Delete (`delete`), the acts `of` that child it names, or\n`all` of them \u2014 never the record\'s acts. A warning reading the figure of a meter that\n`gates` marks against its bound or its pass mark is refused: the meter already draws it.\n\nThe check field\'s `label` is its short name \u2014 "Over budget", "Release expired": a row\'s\nline says it in the check\'s tone (the most severe of several, and how many more), while\nits words whole are the row\'s tooltip and stand under the record\'s header. So label it\nas the reader names the problem, in a few words, and let a text formula say the\nsentence. The compiler records the fields a formula reads: a row drawing each of them\nas a control says nothing of the check on its line (the empty control says it), and a\ncheck with no `resolve` links to the first of them the record draws.\n\n## What a run remembers\n\nThe WORKSPACE remembers what each entity, field, select option, role and\ntemplate alias became here, plus which record each first row landed on and the\nfile each document path was uploaded as. The model file itself holds no live id\n\u2014 it is the portable half, and the same file applies to a demo workspace and to\na customer\'s \u2014 so the join lives where the things it names live, and one model\napplied to two workspaces holds two bindings that know nothing of each other.\n\nIt is the workspace\'s and not the file\'s because a model file is never\ncommitted: memory kept beside it is one author\'s disk, absent for a teammate, on\na second machine or after a delete \u2014 and every one of those goes quietly back to\nbinding by label, which is what grows the second table.\n\nEvery later apply binds through it: it takes the remembered id first and falls\nback to a label only for an alias nothing has bound \u2014 a field you have just\nadded, or a workspace nothing has applied this model to. That is what makes a\nrelabel on EITHER side a rename rather than one thing the workspace lacks and\none the model lacks: `apply` binds the thing it has always meant and reports the\nmove \u2014 which side is right is yours to decide, not a reason to refuse the run.\n\nA bound target the workspace no longer holds IS a refusal, by name: re-binding\nto whatever carries that label today is how the model comes to point at\nsomebody else\'s table. On the CLI, `lotics run restore_table` puts a deleted table back.\n\n`lotics model pull` writes a workspace that already works as a model file \u2014 the\nstarting point for another business\'s model, never a source of truth: it carries\none business\'s words and stops describing that workspace the moment either\nchanges.\n\n## A complete model\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "logo", "label": "Logo", "type": "files" },\n {\n "alias": "tier",\n "label": "Tier",\n "type": "select",\n "options": [\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "default": ["standard"]\n },\n {\n "alias": "orders",\n "label": "Orders",\n "type": "select_record_link",\n "target_entity": "order",\n "cardinality": "many",\n "sync_both_ways": true,\n "paired_field_alias": "customer",\n "display_field_aliases": ["code"]\n },\n {\n "alias": "total_ordered",\n "label": "Total ordered",\n "type": "rollup",\n "source_field_alias": "orders",\n "aggregate_option": { "operation": "sum", "field_key": "amount" }\n }\n ],\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "type": "select",\n "field_key": "tier",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{ "field_key": "name", "order": "asc" }]\n }\n ]\n },\n {\n "alias": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": [\n { "alias": "code", "label": "Order no.", "type": "text", "unique": true },\n { "alias": "placed_on", "label": "Placed on", "type": "date", "format": "date" },\n {\n "alias": "amount",\n "label": "Amount",\n "type": "number",\n "notation": { "style": "currency", "currency": "VND" }\n },\n {\n "alias": "total",\n "label": "Total with VAT",\n "type": "formula",\n "formula": { "expression": "{amount} * 1.1", "notation": { "style": "currency", "currency": "VND" } }\n },\n {\n "alias": "customer",\n "label": "Customer",\n "type": "select_record_link",\n "target_entity": "customer",\n "cardinality": "one",\n "sync_both_ways": true,\n "paired_field_alias": "orders",\n "display_field_aliases": ["name"]\n },\n { "alias": "note", "label": "Note", "type": "text" }\n ]\n }\n ],\n "roles": [{ "alias": "sales", "label": "Sales" }],\n "records": {\n "customer": { "title": "name", "image": "logo", "party": "organization", "figure": "total_ordered" },\n "order": { "title": "customer", "subtitle": ["code", "placed_on"], "figure": "amount", "starts": { "placed_on": "today" } }\n },\n "apps": [\n {\n "alias": "customers",\n "name": "Customers",\n "entity": "customer",\n "register": {\n "columns": ["tier"],\n "readings": [{ "breakdown": "customer", "by": "tier", "value": "total_ordered" }]\n },\n "record": {\n "sections": [\n { "title": "Account", "fields": ["tier"] },\n { "title": "Orders", "blocks": [{ "rows": "order", "columns": ["total"], "create": ["code", "amount"] }] }\n ]\n }\n },\n {\n "alias": "orders",\n "name": "Orders",\n "entity": "order",\n "register": {\n "filters": ["customer"],\n "create": ["code", "customer", "placed_on"],\n "readings": [{ "trend": "order", "over": "placed_on", "value": "amount" }]\n },\n "record": { "door": "drawer", "sections": [{ "title": "Order", "fields": ["note"] }] }\n }\n ],\n "rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } },\n { "ref": "bluebird", "fields": { "name": "Bluebird Foods", "tier": "standard" } }\n ],\n "order": [\n {\n "ref": "so_1001",\n "fields": {\n "code": "SO-1001",\n "placed_on": "@month-start+2",\n "amount": 4200000,\n "customer": "customer:acme"\n }\n },\n {\n "ref": "so_1002",\n "fields": {\n "code": "SO-1002",\n "placed_on": "@today-3",\n "amount": 1150000,\n "customer": "customer:bluebird"\n }\n }\n ]\n }\n}\n```\n\nEach app of this file, as its reader will see it:\n\n```\nApp customers \u2014 "Customers" over Customers (customer)\n Register \u2014 table (default), newest first (default)\n row [Logo] \xB7 Name \xB7 figure Total ordered\n columns Tier\n filters \u2014\n readings "Tier" \u2014 Total ordered summed per Tier, over the rows in view (default)\n add Name (default)\n Record \u2014 a drawer (default)\n header image Logo \xB7 title Name \xB7 figure Total ordered\n checks \u2014\n header \u22EF Delete\n sections 1. "Account"\n fields Tier\n 2. "Orders"\n block Orders (default) \u2014 rows of Orders through Customer; columns Total with VAT; add asks Order no., Amount\n thread \u2014\n history \u2014\n acts \u2014\n Reads Customers, Orders\n\nApp orders \u2014 "Orders" over Orders (order)\n Register \u2014 table (default), newest first (default)\n row Customer \xB7 under it Order no., Placed on \xB7 figure Amount\n columns \u2014\n filters Customer\n readings "Amount" \u2014 Amount summed per period by Placed on, over the rows in view (default)\n add Order no., Customer, Placed on \u2014 starting Placed on at today\n Record \u2014 a drawer\n header title Customer \xB7 subtitle Order no., Placed on \xB7 figure Amount\n checks \u2014\n header \u22EF Delete\n sections 1. "Order"\n fields Note\n thread \u2014\n history \u2014\n acts \u2014\n Reads Orders, Customers\n\n```\n\nEach `(default)` is a value the model left to the system: the door a record opens\nthrough \u2014 a drawer, since neither record has a stage or more than three sections \u2014 the\norder rows open in, and what an add asks. The order\'s one section holds every field a\nperson writes that the header does not already show. The customer\'s orders\nare a block in its "Orders" section, never its own `Orders` link as a field as well:\none fact, one place. An order is titled by its customer and read by its number under\nit; the block names what its add asks, since a subtitle or a figure is never asked by\ndefault. `Total ordered` is a rollup, so it is read and never asked; the\norders app\'s record opens in a drawer, because its rows are worked one after another.\n';
|
|
44374
44535
|
|
|
44375
44536
|
// docs/examples.md
|
|
44376
44537
|
var examples_default = '# Worked examples\n\nEach section is one treatment, cut from a complete model of an invented business: the job, the keys to\nread, where the treatment is wrong, and the excerpt \u2014 an app the check passes with no finding, as stated;\nthe `records` of every entity it draws; those entities\' fields it names, by what each holds and how it is\ndrawn; and the templates it makes. Left out: labels, descriptions, `required`, `unique`, `default`, a link\'s\npairing, how a computed field computes, and template contents. Choosing among them: the `design` reference.\nEvery key: the `model` reference, at the pages each section names under **Keys**.\n\n## Calendar\n\n**The job.** Book visits on the days they happen, and see a week\'s work at a glance.\n\n**Read.** `register.layout: "calendar"`; each visit stands over its span because its `records` line holds two dates (`scheduled_for`, `ends_at`).\n\n**Keys.** `model/calendar`\n\n**Not when.** Rows booked on a resource that two may not share at once \u2014 that is `lanes`. Rows with a date nobody plans by (an invoice\'s date) stay a table.\n\n```json\n{\n "entities": [\n {\n "alias": "service_visit",\n "fields": [\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "scheduled_for", "type": "date", "format": "datetime"},\n {"alias": "ends_at", "type": "date", "format": "datetime"},\n {"alias": "engineer", "type": "select_record_link", "target_entity": "engineer", "cardinality": "one"},\n {\n "alias": "job",\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n {"alias": "booked_on", "type": "date"},\n {"alias": "parts_ready_on", "type": "date"},\n {"alias": "arrived_on", "type": "date"},\n {"alias": "hours", "type": "number", "notation": {"style": "unit", "unit": "h"}},\n {"alias": "report", "type": "text", "format": "markdown"},\n {"alias": "completed_on", "type": "date"}\n ]\n }\n ],\n "records": {\n "service_visit": {\n "title": "customer",\n "subtitle": ["scheduled_for", "ends_at"],\n "status": {\n "milestones": ["booked_on", {"field": "parts_ready_on", "when": {"job": ["repair"]}}, "arrived_on", "completed_on"],\n "closed": ["completed_on"]\n },\n "figure": "hours",\n "due": ["scheduled_for"]\n }\n },\n "apps": [\n {\n "alias": "visits",\n "name": "Service visits",\n "description": "Engineers\' visits to customers on site, booked and completed by the day.",\n "icon": "calendar",\n "entity": "service_visit",\n "register": {\n "columns": ["engineer", "job"],\n "filters": ["customer"],\n "sort": {"field": "scheduled_for"},\n "layout": "calendar",\n "create": ["customer", "job", "scheduled_for", "ends_at", "engineer"],\n "readings": [\n {"metric": "service_visit", "where": {"job": ["repair"]}, "label": "Repairs"},\n {"breakdown": "service_visit", "by": "engineer", "value": "hours"}\n ]\n },\n "record": {\n "sections": [\n {"title": "Visit", "at": ["booked_on", "parts_ready_on"], "fields": ["job"], "acts": ["reschedule"]},\n {"title": "Report", "at": ["arrived_on"], "fields": ["engineer", "report"], "acts": ["complete"]}\n ]\n },\n "acts": [\n {\n "alias": "complete",\n "label": "Complete visit",\n "requires": ["engineer"],\n "asks": ["hours", {"input": "summary", "label": "What was done", "type": "long_text", "required": true}],\n "set": {"report": "input:summary", "completed_on": "now"}\n },\n {"alias": "reschedule", "label": "Reschedule", "asks": ["scheduled_for", "ends_at"]}\n ]\n }\n ]\n}\n```\n\n## Lanes\n\n**The job.** Put each service visit on an engineer\'s day by the hour, and see at once who is free between visits.\n\n**Read.** `register.layout: "lanes"` with `lanes` naming the one-row link to the resource (the engineer); every engineer stands as a lane, empty or not.\n\n**Keys.** `model/lanes`\n\n**Not when.** The resource is not a row of the model (a free-text room name), or rows never compete for one \u2014 a `calendar` reads them by day.\n\n```json\n{\n "entities": [\n {\n "alias": "service_visit",\n "fields": [\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "scheduled_for", "type": "date", "format": "datetime"},\n {"alias": "ends_at", "type": "date", "format": "datetime"},\n {"alias": "engineer", "type": "select_record_link", "target_entity": "engineer", "cardinality": "one"},\n {\n "alias": "job",\n "type": "select",\n "options": [{"alias": "inspection", "color": "sky"}, {"alias": "repair", "color": "orange"}]\n },\n {"alias": "booked_on", "type": "date"},\n {"alias": "parts_ready_on", "type": "date"},\n {"alias": "arrived_on", "type": "date"},\n {"alias": "hours", "type": "number", "notation": {"style": "unit", "unit": "h"}},\n {"alias": "completed_on", "type": "date"}\n ]\n },\n {\n "alias": "engineer",\n "fields": [\n {"alias": "name", "type": "text"},\n {"alias": "base", "type": "select_record_link", "target_entity": "warehouse", "cardinality": "one"}\n ]\n }\n ],\n "records": {\n "service_visit": {\n "title": "customer",\n "subtitle": ["scheduled_for", "ends_at"],\n "status": {\n "milestones": ["booked_on", {"field": "parts_ready_on", "when": {"job": ["repair"]}}, "arrived_on", "completed_on"],\n "closed": ["completed_on"]\n },\n "figure": "hours",\n "due": ["scheduled_for"]\n },\n "engineer": {"title": "name", "subtitle": ["base"], "party": "person"}\n },\n "apps": [\n {\n "alias": "dispatch",\n "name": "Dispatch",\n "description": "Each engineer\'s visits across the day, and the free time between them to book into.",\n "icon": "wrench",\n "entity": "service_visit",\n "register": {\n "columns": ["job"],\n "search": ["customer"],\n "layout": "lanes",\n "lanes": "engineer",\n "create": ["customer", "job"]\n },\n "record": {"door": "drawer", "sections": [{"title": "Booking", "fields": ["engineer", "job"]}]}\n }\n ]\n}\n```\n\n## Gantt\n\n**The job.** Read every hire as a bar from the day the machine goes out to the day it is due back, against today.\n\n**Read.** `register.layout: "gantt"` and `register.gantt.start`; with no end stated, the bar ends at the entity\'s first `due`.\n\n**Keys.** `model/gantt`\n\n**Not when.** Rows that last a day or less (a visit, a shift) \u2014 `calendar` or `lanes`; a plan with no dates set yet.\n\n```json\n{\n "entities": [\n {\n "alias": "phieu_thue",\n "fields": [\n {\n "alias": "khach_hang",\n "type": "select_record_link",\n "target_entity": "khach_hang",\n "cardinality": "one"\n },\n {"alias": "so_phieu", "type": "text"},\n {\n "alias": "trang_thai",\n "type": "select",\n "options": [\n {"alias": "moi", "color": "slate"},\n {"alias": "dang_thue", "color": "blue"},\n {"alias": "da_tra", "color": "green"},\n {"alias": "huy", "color": "gray"}\n ]\n },\n {"alias": "phu_trach", "type": "select_member"},\n {"alias": "ngay_giao", "type": "date"},\n {"alias": "han_tra", "type": "date"},\n {"alias": "ngay_tra", "type": "date"},\n {\n "alias": "tinh_trang_tra",\n "type": "select",\n "options": [\n {"alias": "tot", "color": "green"},\n {"alias": "tray_xuoc", "color": "amber"},\n {"alias": "hu_hong", "color": "red"}\n ]\n },\n {"alias": "so_ngay", "type": "formula"},\n {"alias": "tong_tien", "type": "rollup"}\n ]\n }\n ],\n "records": {\n "phieu_thue": {\n "title": "khach_hang",\n "subtitle": ["so_phieu"],\n "status": {"field": "trang_thai", "closed": ["da_tra", "huy"], "history": "lich_su_phieu"},\n "figure": "tong_tien",\n "due": ["han_tra"]\n }\n },\n "apps": [\n {\n "alias": "tien_do_thue",\n "name": "Ti\u1EBFn \u0111\u1ED9 cho thu\xEA",\n "description": "M\u1ED7i phi\u1EBFu thu\xEA m\u1ED9t thanh t\u1EEB ng\xE0y giao m\xE1y \u0111\u1EBFn h\u1EA1n tr\u1EA3, \u0111\u1ECDc theo h\xF4m nay: phi\u1EBFu n\xE0o s\u1EAFp \u0111\u1EBFn h\u1EA1n, phi\u1EBFu n\xE0o qu\xE1 h\u1EA1n; k\xE9o thanh \u0111\u1EC3 d\u1EDDi ng\xE0y giao ho\u1EB7c gia h\u1EA1n tr\u1EA3, v\xE0 nh\u1EADn m\xE1y v\u1EC1 khi kh\xE1ch tr\u1EA3.",\n "icon": "calendar-range",\n "theme": {"color": "teal"},\n "entity": "phieu_thue",\n "register": {\n "columns": ["phu_trach"],\n "filters": ["phu_trach", "khach_hang"],\n "layout": "gantt",\n "gantt": {"start": "ngay_giao"},\n "remove": false,\n "create": false,\n "readings": [\n {"metric": "phieu_thue", "where": {"trang_thai": ["dang_thue"]}, "label": "Phi\u1EBFu \u0111ang cho thu\xEA"},\n {\n "metric": "phieu_thue",\n "value": "tong_tien",\n "where": {"trang_thai": ["dang_thue"]},\n "label": "Ti\u1EC1n thu\xEA \u0111ang ch\u1EA1y"\n }\n ]\n },\n "record": {\n "sections": [\n {"title": "Phi\u1EBFu thu\xEA", "fields": ["ngay_giao", "han_tra"]},\n {"title": "\u0110ang thu\xEA", "at": ["dang_thue"], "fields": ["so_ngay", "ngay_tra"], "acts": ["nhan_tra"]}\n ]\n },\n "acts": [\n {\n "alias": "nhan_tra",\n "label": "Nh\u1EADn m\xE1y tr\u1EA3",\n "when": {"trang_thai": ["dang_thue"]},\n "asks": ["tinh_trang_tra"],\n "set": {"trang_thai": "da_tra", "ngay_tra": "now"},\n "confirm": true\n }\n ]\n }\n ]\n}\n```\n\n## Roster\n\n**The job.** See who works which shift on each day of the week or the month.\n\n**Read.** `register.layout: "roster"` over the people; `register.roster` names the child whose rows fill the days \u2014 one link back and a date on its line.\n\n**Keys.** `model/roster`\n\n**Not when.** Rows that are themselves the bookings (a stay over several days) \u2014 a `calendar` or `lanes` draws them; a roster\'s row is what the days are filled for.\n\n```json\n{\n "entities": [\n {\n "alias": "nha_si",\n "fields": [\n {"alias": "ho_ten", "type": "text"},\n {\n "alias": "chuyen_mon",\n "type": "select",\n "options": [\n {"alias": "tong_quat", "color": "blue"},\n {"alias": "chinh_nha", "color": "purple"},\n {"alias": "nha_chu", "color": "green"},\n {"alias": "phuc_hinh", "color": "orange"}\n ]\n },\n {"alias": "dien_thoai", "type": "text"}\n ]\n },\n {\n "alias": "lich_truc",\n "fields": [\n {"alias": "nha_si", "type": "select_record_link", "target_entity": "nha_si", "cardinality": "one"},\n {\n "alias": "ca",\n "type": "select",\n "options": [\n {"alias": "sang", "color": "blue"},\n {"alias": "chieu", "color": "purple"},\n {"alias": "ca_ngay", "color": "green"},\n {"alias": "nghi", "color": "gray"}\n ]\n },\n {"alias": "ngay", "type": "date"},\n {"alias": "ghe", "type": "select_record_link", "target_entity": "ghe", "cardinality": "one"}\n ]\n }\n ],\n "records": {\n "nha_si": {"title": "ho_ten", "subtitle": ["chuyen_mon"], "party": "person"},\n "lich_truc": {"title": "ca", "subtitle": ["ngay", "ghe"]}\n },\n "apps": [\n {\n "alias": "lich_truc",\n "name": "L\u1ECBch tr\u1EF1c",\n "description": "Ai tr\u1EF1c ca n\xE0o, \u1EDF gh\u1EBF n\xE0o, trong tu\u1EA7n v\xE0 trong th\xE1ng.",\n "icon": "users",\n "entity": "nha_si",\n "register": {\n "filters": ["chuyen_mon"],\n "layout": "roster",\n "roster": "lich_truc",\n "create": ["ho_ten", "chuyen_mon", "dien_thoai"]\n }\n }\n ]\n}\n```\n\n## Cards\n\n**The job.** Find an item by its picture, and count what the shelf holds.\n\n**Read.** `register.layout: "cards"` over rows whose `records.image` is a photo; `scope` keeps the app inside one warehouse at a time.\n\n**Keys.** `model/cards`, `model/records`, `model/apps`\n\n**Not when.** Rows with no picture \u2014 cards of words are a table drawn worse.\n\n```json\n{\n "entities": [\n {\n "alias": "stock_item",\n "fields": [\n {"alias": "name", "type": "text"},\n {\n "alias": "warehouse",\n "type": "select_record_link",\n "target_entity": "warehouse",\n "cardinality": "one"\n },\n {"alias": "photo", "type": "files"},\n {"alias": "sku", "type": "text"},\n {\n "alias": "category",\n "type": "select",\n "options": [\n {"alias": "pumps", "color": "blue"},\n {"alias": "valves", "color": "teal"},\n {"alias": "seals", "color": "amber"},\n {"alias": "motors", "color": "violet"}\n ]\n },\n {"alias": "on_hand", "type": "number"},\n {"alias": "minimum", "type": "number"},\n {"alias": "price", "type": "number", "notation": {"style": "currency", "currency": "USD"}},\n {"alias": "supplier_page", "type": "text", "format": "link"},\n {"alias": "last_counted", "type": "date"},\n {"alias": "counted_by", "type": "select_member"},\n {"alias": "count_note", "type": "text"}\n ]\n },\n {\n "alias": "order_line",\n "fields": [\n {"alias": "item", "type": "select_record_link", "target_entity": "stock_item", "cardinality": "one"},\n {"alias": "order", "type": "select_record_link", "target_entity": "order", "cardinality": "one"},\n {"alias": "quantity", "type": "number"},\n {"alias": "shipped", "type": "number"},\n {"alias": "picked", "type": "number"},\n {"alias": "unit_price", "type": "number", "notation": {"style": "currency", "currency": "USD"}},\n {"alias": "amount", "type": "formula"},\n {"alias": "category", "type": "lookup"}\n ]\n }\n ],\n "records": {\n "stock_item": {\n "title": "name",\n "subtitle": ["sku", "category"],\n "image": "photo",\n "figure": "on_hand",\n "limits": {"on_hand": 400},\n "gates": {"on_hand": "minimum"}\n },\n "order_line": {\n "title": "item",\n "subtitle": ["order", "unit_price"],\n "figure": "amount",\n "limits": {"shipped": "quantity", "picked": "quantity"}\n }\n },\n "apps": [\n {\n "alias": "stock",\n "name": "Stock",\n "description": "What the store holds of each item, against its shelf and its minimum.",\n "icon": "boxes",\n "theme": {"color": "teal"},\n "entity": "stock_item",\n "scope": "warehouse",\n "register": {\n "columns": ["price"],\n "filters": ["on_hand"],\n "search": ["name", "sku"],\n "layout": "cards",\n "create": false,\n "export": true,\n "readings": [{"breakdown": "stock_item", "by": "category", "value": "on_hand"}]\n },\n "record": {\n "sections": [\n {"title": "Item", "fields": ["price", "minimum", "supplier_page"]},\n {"title": "Last count", "fields": ["last_counted", "counted_by", "count_note"], "acts": ["count"]},\n {"title": "On order", "blocks": [{"rows": "order_line", "columns": ["quantity"]}]}\n ]\n },\n "acts": [\n {\n "alias": "count",\n "label": "Count stock",\n "asks": [\n {"input": "counted", "label": "Units on the shelf", "type": "number", "required": true},\n {"input": "remark", "label": "Remark", "type": "long_text"}\n ],\n "set": {\n "on_hand": "input:counted",\n "last_counted": "now",\n "counted_by": "me",\n "count_note": "input:remark"\n }\n }\n ]\n }\n ]\n}\n```\n\n## Party\n\n**The job.** Keep each customer account \u2014 who they are, what they ordered, what was said to them.\n\n**Read.** `records.customer.party: "organization"` and its `image` (the logo); a `timeline` of touches, a `trend` of its orders, and its papers `expect`ed by kind.\n\n**Keys.** `model/records`, `model/timeline-and-agenda`, `model/blocks`, `model/reading-blocks`\n\n**Not when.** A list of customers is not a job by itself: the customer is a link on the desk whose rows name it, unless the job is the account itself.\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "fields": [\n {"alias": "name", "type": "text"},\n {"alias": "logo", "type": "files"},\n {"alias": "code", "type": "text"},\n {\n "alias": "tier",\n "type": "select",\n "options": [\n {"alias": "key", "color": "violet"},\n {"alias": "standard", "color": "sky"},\n {"alias": "lapsed", "color": "gray"}\n ]\n },\n {"alias": "phone", "type": "text"},\n {"alias": "website", "type": "text", "format": "link"},\n {"alias": "account_manager", "type": "select_member"},\n {"alias": "credit_limit", "type": "number", "notation": {"style": "currency", "currency": "USD"}},\n {"alias": "outstanding", "type": "rollup"},\n {"alias": "over_limit", "type": "formula"},\n {\n "alias": "papers_needed",\n "type": "select",\n "options": [\n {"alias": "credit_application", "color": "blue"},\n {"alias": "trade_reference", "color": "violet"},\n {"alias": "vat_certificate", "color": "teal"}\n ],\n "multi": true\n },\n {"alias": "notes", "type": "text", "format": "markdown"}\n ]\n },\n {\n "alias": "order",\n "fields": [\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "reference", "type": "text"},\n {\n "alias": "stage",\n "type": "select",\n "options": [\n {"alias": "draft", "color": "slate"},\n {"alias": "confirmed", "color": "blue"},\n {"alias": "picking", "color": "amber"},\n {"alias": "shipped", "color": "indigo"},\n {"alias": "delivered", "color": "green"},\n {"alias": "cancelled", "color": "rose"}\n ]\n },\n {"alias": "placed", "type": "date"},\n {"alias": "due_date", "type": "date"},\n {"alias": "total", "type": "rollup"},\n {"alias": "balance", "type": "formula"},\n {"alias": "units_ordered", "type": "rollup"},\n {"alias": "units_shipped", "type": "rollup"},\n {"alias": "units_picked", "type": "rollup"},\n {"alias": "notes", "type": "text", "format": "markdown"},\n {"alias": "paid_in_full", "type": "formula"}\n ]\n },\n {\n "alias": "customer_paper",\n "fields": [\n {\n "alias": "kind",\n "type": "select",\n "options": [\n {"alias": "credit_application", "color": "blue"},\n {"alias": "trade_reference", "color": "violet"},\n {"alias": "vat_certificate", "color": "teal"}\n ]\n },\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "received_on", "type": "date"},\n {"alias": "file", "type": "files"}\n ]\n },\n {\n "alias": "touch",\n "fields": [\n {"alias": "said", "type": "text"},\n {"alias": "customer", "type": "select_record_link", "target_entity": "customer", "cardinality": "one"},\n {"alias": "by", "type": "select_member"}\n ]\n }\n ],\n "records": {\n "customer": {\n "title": "name",\n "subtitle": ["tier"],\n "image": "logo",\n "party": "organization",\n "figure": "outstanding",\n "limits": {"outstanding": "credit_limit"}\n },\n "order": {\n "title": "reference",\n "subtitle": ["customer", "placed"],\n "status": {"field": "stage", "closed": ["delivered", "cancelled"], "history": "order_stage"},\n "figure": "total",\n "limits": {"units_shipped": "units_ordered", "units_picked": "units_ordered"},\n "due": ["due_date"]\n },\n "customer_paper": {"title": "kind", "image": "file"},\n "touch": {"title": "said"}\n },\n "apps": [\n {\n "alias": "customers",\n "name": "Customers",\n "description": "Who we sell to, what they owe, and what was said.",\n "icon": "users",\n "theme": {"color": "violet"},\n "entity": "customer",\n "reads": "shared",\n "register": {\n "columns": ["phone", "account_manager"],\n "filters": ["account_manager"],\n "sort": {"field": "name"},\n "opens": {"tier": ["key", "standard"]},\n "create": ["name", "code", "credit_limit"],\n "readings": [\n {"breakdown": "customer", "by": "tier", "value": "outstanding"},\n {"breakdown": "customer", "by": "account_manager", "value": "outstanding"}\n ]\n },\n "record": {\n "door": "drawer",\n "sections": [\n {"title": "Contact", "fields": ["phone", "website"]},\n {\n "title": "Account",\n "fields": ["code", "account_manager", "credit_limit"],\n "acts": ["reassign", "lapse", "reinstate"]\n },\n {\n "title": "Orders",\n "blocks": [\n {\n "trend": "order",\n "over": "placed",\n "value": "total",\n "where": {"stage": ["confirmed", "picking", "shipped", "delivered"]},\n "label": "Order value"\n },\n {\n "rows": "order",\n "via": "customer",\n "title": "Open orders",\n "columns": ["due_date"],\n "create": false,\n "where": {"stage": ["draft", "confirmed", "picking", "shipped"]}\n },\n {\n "rows": "order",\n "via": "customer",\n "title": "Shipped, still owed",\n "columns": ["balance"],\n "create": false,\n "where": {"stage": ["shipped", "delivered"], "paid_in_full": false}\n }\n ]\n },\n {\n "title": "Account papers",\n "blocks": [\n {\n "rows": "customer_paper",\n "via": "customer",\n "columns": ["received_on"],\n "expect": "kind",\n "of": "papers_needed"\n }\n ]\n },\n {"title": "Contact log", "blocks": [{"timeline": "touch", "via": "customer"}]},\n {"title": "Notes", "blocks": [{"text": "notes"}]}\n ]\n },\n "acts": [\n {\n "alias": "lapse",\n "label": "Mark lapsed",\n "when": {"tier": ["key", "standard"]},\n "set": {"tier": "lapsed"},\n "confirm": true\n },\n {\n "alias": "reinstate",\n "label": "Reinstate",\n "when": {"tier": ["lapsed"]},\n "asks": [\n {\n "input": "back_to",\n "label": "Back to",\n "type": "select",\n "options": [\n {"alias": "key", "label": "Key account", "color": "violet"},\n {"alias": "standard", "label": "Standard", "color": "sky"}\n ],\n "required": true\n }\n ],\n "set": {"tier": "input:back_to"}\n },\n {\n "alias": "reassign",\n "label": "Reassign",\n "asks": [{"input": "to", "label": "Account manager", "type": "member", "required": true}],\n "set": {"account_manager": "input:to"}\n }\n ],\n "checks": [{"field": "over_limit"}]\n }\n ]\n}\n```\n\n## Case\n\n**The job.** Gather an applicant\'s papers into one case, make the forms it needs, and file it on time.\n\n**Read.** `expect` blocks for the papers brought (one line per kind, the missing ones empty), an act\'s `templates` for the forms made (ticked and made at once, kept in `into`), and `intake` reading the papers brought.\n\n**Keys.** `model/blocks`, `model/papers-and-recordings`\n\n**Not when.** A record that needs no papers, or one paper made once \u2014 a single `template` act.\n\n```json\n{\n "entities": [\n {\n "alias": "ho_so",\n "fields": [\n {\n "alias": "khach_hang",\n "type": "select_record_link",\n "target_entity": "khach_hang",\n "cardinality": "one"\n },\n {"alias": "du_an", "type": "select_record_link", "target_entity": "du_an", "cardinality": "one"},\n {"alias": "ma_ho_so", "type": "autonumber"},\n {\n "alias": "dich_vu",\n "type": "select",\n "options": [{"alias": "tron_goi", "color": "blue"}, {"alias": "lam_ho_so", "color": "violet"}]\n },\n {\n "alias": "doi_tuong",\n "type": "select",\n "options": [\n {"alias": "thu_nhap_thap", "color": "sky"},\n {"alias": "cong_nhan", "color": "teal"},\n {"alias": "can_bo", "color": "indigo"},\n {"alias": "luc_luong", "color": "green"},\n {"alias": "ho_ngheo", "color": "amber"},\n {"alias": "nguoi_co_cong", "color": "rose"}\n ]\n },\n {\n "alias": "tinh_trang_nha_o",\n "type": "select",\n "options": [{"alias": "chua_co_nha", "color": "gray"}, {"alias": "nha_chat", "color": "orange"}]\n },\n {"alias": "giay_chung_nhan_so", "type": "text"},\n {"alias": "dien_tich_nha", "type": "number"},\n {"alias": "sale", "type": "select_member"},\n {"alias": "xu_ly", "type": "select_member"},\n {"alias": "can_bo_sung", "type": "boolean"},\n {"alias": "phi_dich_vu", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {\n "alias": "loai_can",\n "type": "select",\n "options": [\n {"alias": "studio", "color": "sky"},\n {"alias": "mot_pn", "color": "blue"},\n {"alias": "hai_pn", "color": "indigo"},\n {"alias": "ba_pn", "color": "violet"}\n ]\n },\n {"alias": "can_boc", "type": "text"},\n {"alias": "hoa_hong_sale", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {"alias": "ngay_thu_phi", "type": "date"},\n {"alias": "ngay_nhan_giay_to", "type": "date"},\n {"alias": "ngay_lap_ho_so", "type": "date"},\n {"alias": "ngay_nop_cdt", "type": "date"},\n {"alias": "ngay_cdt_duyet", "type": "date"},\n {"alias": "ngay_so_xd_duyet", "type": "date"},\n {"alias": "ngay_boc_tham", "type": "date"},\n {"alias": "ngay_ky_hdmb", "type": "date"},\n {"alias": "ngay_ban_giao", "type": "date"},\n {"alias": "bo_ho_so", "type": "files"},\n {"alias": "hop_dong_mua_ban", "type": "files"},\n {"alias": "giay_to", "type": "select_record_link", "target_entity": "giay_to", "cardinality": "many"},\n {\n "alias": "thanh_vien",\n "type": "select_record_link",\n "target_entity": "thanh_vien",\n "cardinality": "many"\n },\n {\n "alias": "phieu_thu",\n "type": "select_record_link",\n "target_entity": "phieu_thu",\n "cardinality": "many"\n },\n {"alias": "dien_thoai", "type": "lookup"},\n {"alias": "cccd", "type": "lookup"},\n {"alias": "ngay_sinh", "type": "lookup"},\n {"alias": "dia_chi", "type": "lookup"},\n {"alias": "da_thu", "type": "rollup"},\n {"alias": "chua_thu_phi", "type": "formula"},\n {"alias": "con_no_phi", "type": "formula"},\n {"alias": "vuot_thu_nhap", "type": "formula"},\n {"alias": "da_hoan_tat", "type": "formula"}\n ]\n },\n {\n "alias": "thanh_vien",\n "fields": [\n {"alias": "ho_ten", "type": "text"},\n {"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},\n {\n "alias": "quan_he",\n "type": "select",\n "options": [\n {"alias": "nguoi_dung_don", "color": "blue"},\n {"alias": "vo_chong", "color": "violet"},\n {"alias": "con", "color": "teal"},\n {"alias": "bo_me", "color": "amber"},\n {"alias": "khac", "color": "gray"}\n ]\n },\n {"alias": "ngay_sinh", "type": "date"},\n {"alias": "cccd", "type": "text"},\n {"alias": "thu_nhap", "type": "number", "notation": {"style": "currency", "currency": "VND"}}\n ]\n },\n {\n "alias": "phieu_thu",\n "fields": [\n {\n "alias": "khoan",\n "type": "select",\n "options": [\n {"alias": "dat_coc", "color": "blue"},\n {"alias": "tat_toan", "color": "indigo"},\n {"alias": "phi_ho_so", "color": "violet"}\n ]\n },\n {"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},\n {"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {\n "alias": "hinh_thuc",\n "type": "select",\n "options": [\n {"alias": "tien_mat", "color": "green", "mark": {"kind": "icon", "name": "banknote"}},\n {"alias": "chuyen_khoan", "color": "blue", "mark": {"kind": "icon", "name": "arrow-right-left"}}\n ]\n },\n {\n "alias": "trang_thai",\n "type": "select",\n "options": [\n {"alias": "cho_thu", "color": "amber"},\n {"alias": "da_thu", "color": "green"},\n {"alias": "huy", "color": "gray"}\n ]\n },\n {"alias": "ngay_thu", "type": "date"},\n {"alias": "nguoi_thu", "type": "select_member"},\n {"alias": "da_chot", "type": "formula"}\n ]\n },\n {\n "alias": "giay_to",\n "fields": [\n {\n "alias": "loai",\n "type": "select",\n "options": [\n {"alias": "don_dang_ky", "color": "blue"},\n {"alias": "can_cuoc", "color": "sky"},\n {"alias": "giay_doi_tuong", "color": "violet"},\n {"alias": "xac_nhan_nha_o", "color": "teal"},\n {"alias": "xac_nhan_thu_nhap", "color": "amber"},\n {"alias": "hon_nhan", "color": "indigo"}\n ]\n },\n {"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},\n {"alias": "ngay_nhan", "type": "date"},\n {"alias": "can_sua", "type": "text"},\n {"alias": "tep", "type": "files"},\n {"alias": "phai_sua", "type": "formula"}\n ]\n }\n ],\n "records": {\n "ho_so": {\n "title": "khach_hang",\n "subtitle": ["dich_vu"],\n "status": {\n "milestones": [\n {"field": "ngay_thu_phi", "when": {"dich_vu": ["lam_ho_so"]}},\n "ngay_nhan_giay_to",\n {"field": "ngay_lap_ho_so", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_nop_cdt", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_cdt_duyet", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_so_xd_duyet", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_boc_tham", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_ky_hdmb", "when": {"dich_vu": ["tron_goi"]}},\n {"field": "ngay_ban_giao", "when": {"dich_vu": ["lam_ho_so"]}}\n ],\n "closed": ["ngay_ky_hdmb", "ngay_ban_giao"]\n },\n "figure": "phi_dich_vu"\n },\n "thanh_vien": {"title": "ho_ten", "subtitle": ["quan_he", "cccd"], "party": "person", "figure": "thu_nhap"},\n "phieu_thu": {\n "title": "khoan",\n "subtitle": ["hinh_thuc", "ngay_thu"],\n "status": {"field": "trang_thai", "closed": ["da_thu", "huy"]},\n "figure": "so_tien"\n },\n "giay_to": {"title": "loai", "image": "tep", "starts": {"ngay_nhan": "today"}}\n },\n "templates": [\n {"alias": "don_dang_ky", "type": "html", "label": "\u0110\u01A1n \u0111\u0103ng k\xFD mua nh\xE0 \u1EDF x\xE3 h\u1ED9i"},\n {"alias": "mau_02_chua_co_nha", "type": "html", "label": "M\u1EABu 02 \u2014 X\xE1c nh\u1EADn ch\u01B0a c\xF3 nh\xE0 \u1EDF"},\n {"alias": "mau_03_nha_chat", "type": "html", "label": "M\u1EABu 03 \u2014 X\xE1c nh\u1EADn nh\xE0 \u1EDF d\u01B0\u1EDBi 15 m\xB2/ng\u01B0\u1EDDi"},\n {"alias": "mau_05_thu_nhap", "type": "html", "label": "M\u1EABu 05 \u2014 X\xE1c nh\u1EADn thu nh\u1EADp"},\n {"alias": "danh_sach_nop", "type": "html", "label": "Danh s\xE1ch h\u1ED3 s\u01A1 n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0"}\n ],\n "apps": [\n {\n "alias": "ho_so",\n "name": "H\u1ED3 s\u01A1 nh\xE0 \u1EDF x\xE3 h\u1ED9i",\n "description": "B\xE0n l\xE0m h\u1ED3 s\u01A1 c\u1EE7a t\u1EEBng d\u1EF1 \xE1n: nh\u1EADn gi\u1EA5y t\u1EDD, l\u1EADp v\xE0 n\u1ED9p h\u1ED3 s\u01A1 cho ch\u1EE7 \u0111\u1EA7u t\u01B0, theo t\u1EEBng kh\xE2u duy\u1EC7t t\u1EDBi b\u1ED1c th\u0103m v\xE0 k\xFD h\u1EE3p \u0111\u1ED3ng \u2014 ho\u1EB7c l\xE0m h\u1ED3 s\u01A1 r\u1ED3i b\xE0n giao cho kh\xE1ch t\u1EF1 n\u1ED9p.",\n "icon": "folder-open",\n "theme": {"color": "blue"},\n "entity": "ho_so",\n "scope": "du_an",\n "register": {\n "columns": ["xu_ly", "can_bo_sung", "doi_tuong"],\n "filters": ["xu_ly", "can_bo_sung"],\n "search": ["khach_hang", "ma_ho_so", "dien_thoai"],\n "create": ["khach_hang", "dich_vu", "doi_tuong", "tinh_trang_nha_o", "loai_can", "phi_dich_vu", "sale"],\n "export": true,\n "readings": [\n {"metric": "ho_so", "where": {"can_bo_sung": true}, "label": "Ch\u1EDD kh\xE1ch b\u1ED5 sung"},\n {"metric": "ho_so", "value": "da_thu", "over": "ngay_nhan_giay_to", "label": "Ph\xED \u0111\xE3 thu"},\n {"breakdown": "ho_so", "by": "doi_tuong"}\n ]\n },\n "record": {\n "sections": [\n {\n "title": "Ng\u01B0\u1EDDi \u0111\u1EE9ng \u0111\u01A1n",\n "fields": [\n "ma_ho_so",\n "dien_thoai",\n "cccd",\n "ngay_sinh",\n "dia_chi",\n "doi_tuong",\n "tinh_trang_nha_o",\n "sale",\n "xu_ly",\n "can_bo_sung"\n ],\n "blocks": [\n {\n "rows": "thanh_vien",\n "title": "H\u1ED9 gia \u0111\xECnh",\n "columns": ["ngay_sinh"],\n "create": ["ho_ten", "quan_he", "ngay_sinh", "cccd", "thu_nhap"]\n }\n ],\n "acts": ["doc_giay_to", "giao_xu_ly", "chuyen_du_an"]\n },\n {\n "title": "Nh\xE0 hi\u1EC7n c\xF3",\n "fields": ["giay_chung_nhan_so", "dien_tich_nha"],\n "when": {"tinh_trang_nha_o": ["nha_chat"]}\n },\n {\n "title": "Ph\xED d\u1ECBch v\u1EE5",\n "blocks": [\n {\n "rows": "phieu_thu",\n "title": "Phi\u1EBFu thu",\n "columns": ["so_tien"],\n "create": ["khoan", "so_tien", "hinh_thuc"],\n "where": {"khoan": ["dat_coc", "tat_toan"]},\n "expect": "khoan",\n "when": {"dich_vu": ["tron_goi"]}\n },\n {\n "rows": "phieu_thu",\n "title": "Phi\u1EBFu thu",\n "columns": ["so_tien"],\n "create": ["khoan", "so_tien", "hinh_thuc"],\n "where": {"khoan": ["phi_ho_so"]},\n "expect": "khoan",\n "when": {"dich_vu": ["lam_ho_so"]}\n }\n ]\n },\n {\n "title": "Gi\u1EA5y t\u1EDD",\n "at": ["ngay_thu_phi"],\n "blocks": [\n {\n "rows": "giay_to",\n "title": "Gi\u1EA5y t\u1EDD",\n "columns": ["ngay_nhan"],\n "create": ["loai", "ngay_nhan", "can_sua", "tep"],\n "expect": "loai"\n }\n ]\n },\n {\n "title": "B\u1ED9 h\u1ED3 s\u01A1",\n "description": "C\xE1c m\u1EABu \u0111\u01A1n in t\u1EEB th\xF4ng tin c\u1EE7a h\u1ED3 s\u01A1.",\n "at": ["ngay_nhan_giay_to", "ngay_lap_ho_so"],\n "acts": ["tao_giay_to", "nop_cdt"]\n },\n {\n "title": "Ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "description": "H\u1ED3 s\u01A1 tr\u1ECDn g\xF3i n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0, ch\u1EDD ch\u1EE7 \u0111\u1EA7u t\u01B0 r\u1ED3i S\u1EDF X\xE2y d\u1EF1ng duy\u1EC7t tr\u01B0\u1EDBc ng\xE0y b\u1ED1c th\u0103m.",\n "at": ["ngay_nop_cdt", "ngay_cdt_duyet", "ngay_so_xd_duyet"],\n "fields": ["loai_can"],\n "when": {"dich_vu": ["tron_goi"]}\n },\n {\n "title": "C\u0103n h\u1ED9",\n "at": ["ngay_boc_tham"],\n "fields": ["can_boc", "hoa_hong_sale"],\n "blocks": [{"files": ["hop_dong_mua_ban"]}],\n "when": {"dich_vu": ["tron_goi"]}\n }\n ]\n },\n "acts": [\n {\n "alias": "doc_giay_to",\n "label": "\u0110\u1ECDc gi\u1EA5y t\u1EDD",\n "intake": "giay_to",\n "fills": [\n "doi_tuong",\n "tinh_trang_nha_o",\n "giay_chung_nhan_so",\n "dien_tich_nha",\n "khach_hang.ho_ten",\n "khach_hang.cccd",\n "khach_hang.ngay_sinh",\n "khach_hang.dia_chi",\n "thanh_vien"\n ]\n },\n {\n "alias": "tao_giay_to",\n "label": "T\u1EA1o gi\u1EA5y t\u1EDD",\n "templates": [\n {"template": "don_dang_ky"},\n {"template": "mau_02_chua_co_nha", "when": {"tinh_trang_nha_o": ["chua_co_nha"]}},\n {"template": "mau_03_nha_chat", "when": {"tinh_trang_nha_o": ["nha_chat"]}},\n {"template": "mau_05_thu_nhap", "when": {"doi_tuong": ["thu_nhap_thap"]}}\n ],\n "into": "bo_ho_so"\n },\n {\n "alias": "nop_cdt",\n "label": "N\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "when": {"dich_vu": ["tron_goi"]},\n "requires": ["bo_ho_so", "loai_can"],\n "asks": ["loai_can"],\n "set": {"ngay_nop_cdt": "now"},\n "confirm": true\n },\n {\n "alias": "giao_xu_ly",\n "label": "Giao ng\u01B0\u1EDDi x\u1EED l\xFD",\n "asks": [{"input": "nguoi", "label": "Ng\u01B0\u1EDDi x\u1EED l\xFD", "type": "member", "required": true}],\n "set": {"xu_ly": "input:nguoi"}\n },\n {\n "alias": "chuyen_du_an",\n "label": "Chuy\u1EC3n d\u1EF1 \xE1n",\n "asks": [{"input": "du_an_moi", "label": "Sang d\u1EF1 \xE1n", "type": "link", "entity": "du_an", "required": true}],\n "set": {"du_an": "input:du_an_moi"},\n "confirm": true\n },\n {\n "alias": "in_danh_sach",\n "label": "In danh s\xE1ch n\u1ED9p ch\u1EE7 \u0111\u1EA7u t\u01B0",\n "on": "view",\n "template": "danh_sach_nop"\n },\n {\n "alias": "xac_nhan_thu",\n "label": "X\xE1c nh\u1EADn \u0111\xE3 thu",\n "of": "phieu_thu",\n "when": {"trang_thai": ["cho_thu"]},\n "asks": ["hinh_thuc"],\n "set": {"trang_thai": "da_thu", "ngay_thu": "now", "nguoi_thu": "me"},\n "confirm": true\n },\n {\n "alias": "huy_phieu",\n "label": "H\u1EE7y phi\u1EBFu",\n "of": "phieu_thu",\n "when": {"trang_thai": ["cho_thu"]},\n "set": {"trang_thai": "huy"},\n "danger": true\n }\n ],\n "checks": [\n {"field": "chua_thu_phi", "blocks": ["nop_cdt"]},\n {"field": "con_no_phi"},\n {"field": "vuot_thu_nhap"},\n {"field": "da_hoan_tat", "blocks": ["delete", "doc_giay_to"]},\n {"field": "da_chot", "of": "phieu_thu", "blocks": ["edit"]},\n {"field": "phai_sua", "of": "giay_to"}\n ]\n }\n ]\n}\n```\n\n## Calls\n\n**The job.** Call a lead from their record, and keep what was said as a touch on its timeline.\n\n**Read.** An act\'s `record` (into the `timeline` child: audio, transcript, when and who) and `fills` (the touch\'s fields an agent fills from the transcript).\n\n**Keys.** `model/papers-and-recordings`, `model/timeline-and-agenda`\n\n**Not when.** Calls made and logged elsewhere \u2014 a `timeline` with its own add keeps the note.\n\n```json\n{\n "entities": [\n {\n "alias": "khach_tiem_nang",\n "fields": [\n {"alias": "ho_ten", "type": "text"},\n {"alias": "ten_cong_ty", "type": "text"},\n {\n "alias": "vai_tro",\n "type": "select",\n "options": [\n {"alias": "chu_doanh_nghiep", "color": "emerald"},\n {"alias": "ke_toan_truong", "color": "blue"},\n {"alias": "quan_ly", "color": "teal"},\n {"alias": "nhan_vien", "color": "sky"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n {\n "alias": "loai_hinh",\n "type": "select",\n "options": [\n {"alias": "thuong_mai", "color": "blue"},\n {"alias": "san_xuat", "color": "amber"},\n {"alias": "dich_vu", "color": "violet"},\n {"alias": "xay_dung", "color": "orange"},\n {"alias": "ho_kinh_doanh", "color": "teal"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n {\n "alias": "quy_mo",\n "type": "select",\n "options": [\n {"alias": "q_1_10", "color": "sky"},\n {"alias": "q_11_50", "color": "blue"},\n {"alias": "q_51_200", "color": "indigo"},\n {"alias": "q_200", "color": "violet"},\n {"alias": "chua_ro", "color": "zinc"}\n ]\n },\n {"alias": "so_dien_thoai", "type": "text"},\n {"alias": "zalo", "type": "text"},\n {"alias": "email", "type": "text"},\n {"alias": "trang_ca_nhan", "type": "text", "format": "link"},\n {"alias": "anh", "type": "files"},\n {\n "alias": "trang_thai",\n "type": "select",\n "options": [\n {"alias": "moi", "color": "gray"},\n {"alias": "du_dieu_kien", "color": "blue"},\n {"alias": "da_lien_he", "color": "amber"},\n {"alias": "da_chuyen_doi", "color": "emerald"},\n {"alias": "loai", "color": "red"}\n ]\n },\n {\n "alias": "ly_do_loai",\n "type": "select",\n "options": [\n {"alias": "khong_phu_hop", "color": "orange"},\n {"alias": "qua_nho", "color": "amber"},\n {"alias": "chua_co_ngan_sach", "color": "yellow"},\n {"alias": "sai_nguoi", "color": "blue"},\n {"alias": "yeu_cau_dung", "color": "red"},\n {"alias": "trung", "color": "zinc"}\n ]\n },\n {"alias": "vi_sao_du_dieu_kien", "type": "text"},\n {"alias": "phu_trach", "type": "select_member"},\n {"alias": "nguon", "type": "select_record_link", "target_entity": "kenh", "cardinality": "one"},\n {"alias": "lien_he", "type": "select_record_link", "target_entity": "lien_he", "cardinality": "one"},\n {"alias": "cong_ty", "type": "select_record_link", "target_entity": "cong_ty", "cardinality": "one"},\n {"alias": "ngay_chuyen_doi", "type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n {"alias": "ghi_chu", "type": "text", "format": "markdown"},\n {"alias": "tin_hieu", "type": "select_record_link", "target_entity": "tin_hieu"},\n {"alias": "buoc_tiep", "type": "select_record_link", "target_entity": "buoc_tiep"},\n {"alias": "tuong_tac", "type": "select_record_link", "target_entity": "tuong_tac"},\n {"alias": "tin_hieu_moi", "type": "lookup"},\n {"alias": "do_nong", "type": "formula"},\n {"alias": "han_buoc_tiep", "type": "rollup"},\n {"alias": "khong_lien_he", "type": "formula"}\n ]\n },\n {\n "alias": "tin_hieu",\n "fields": [\n {"alias": "tom_tat", "type": "text"},\n {"alias": "noi_dung", "type": "text", "format": "markdown"},\n {"alias": "anh_chup", "type": "files"},\n {"alias": "lien_ket", "type": "text", "format": "link"},\n {"alias": "kenh", "type": "select_record_link", "target_entity": "kenh", "cardinality": "one"},\n {\n "alias": "loai",\n "type": "select",\n "options": [\n {"alias": "hoi_dich_vu", "color": "emerald"},\n {"alias": "than_phien", "color": "blue"},\n {"alias": "tuyen_ke_toan", "color": "amber"},\n {"alias": "moi_thanh_lap", "color": "violet"},\n {"alias": "gioi_thieu", "color": "teal"},\n {"alias": "danh_ba", "color": "zinc"},\n {"alias": "nhieu", "color": "stone"}\n ]\n },\n {"alias": "ngay_dang", "type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n {"alias": "ngay_xay_ra", "type": "formula"}\n ]\n },\n {\n "alias": "buoc_tiep",\n "fields": [\n {"alias": "vi_sao_luc_nay", "type": "text"},\n {\n "alias": "viec",\n "type": "select",\n "options": [\n {"alias": "binh_luan", "color": "blue", "mark": {"kind": "icon", "name": "message-square"}},\n {"alias": "nhan_facebook", "color": "violet", "mark": {"kind": "brand", "name": "facebook"}},\n {"alias": "nhan_zalo", "color": "sky", "mark": {"kind": "brand", "name": "zalo"}},\n {"alias": "goi_dien", "color": "teal", "mark": {"kind": "icon", "name": "phone"}},\n {"alias": "gui_email", "color": "indigo", "mark": {"kind": "icon", "name": "mail"}},\n {"alias": "gui_bang_gia", "color": "amber", "mark": {"kind": "icon", "name": "receipt"}}\n ]\n },\n {"alias": "ban_nhap", "type": "text", "format": "markdown"},\n {"alias": "han", "type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n {\n "alias": "tinh_trang",\n "type": "select",\n "options": [\n {"alias": "cho", "color": "blue"},\n {"alias": "xong", "color": "emerald"},\n {"alias": "bo_qua", "color": "zinc"}\n ]\n }\n ]\n },\n {\n "alias": "tuong_tac",\n "fields": [\n {"alias": "tom_tat", "type": "text"},\n {"alias": "thoi_diem", "type": "date", "format": "datetime", "timezone": "Asia/Ho_Chi_Minh"},\n {\n "alias": "qua",\n "type": "select",\n "options": [\n {"alias": "goi_dien", "color": "teal", "mark": {"kind": "icon", "name": "phone"}},\n {"alias": "zalo", "color": "sky", "mark": {"kind": "brand", "name": "zalo"}},\n {"alias": "facebook", "color": "blue", "mark": {"kind": "brand", "name": "facebook"}},\n {"alias": "email", "color": "indigo", "mark": {"kind": "icon", "name": "mail"}},\n {"alias": "gap_truc_tiep", "color": "emerald", "mark": {"kind": "icon", "name": "users"}},\n {"alias": "khac", "color": "zinc", "mark": {"kind": "icon", "name": "ellipsis"}}\n ]\n },\n {\n "alias": "phan_hoi",\n "type": "select",\n "options": [\n {"alias": "dang_cho", "color": "blue"},\n {"alias": "da_tra_loi", "color": "green"},\n {"alias": "quan_tam", "color": "amber"},\n {"alias": "khong_phu_hop", "color": "red"},\n {"alias": "khong_phan_hoi", "color": "zinc"},\n {"alias": "sai_so", "color": "stone"}\n ]\n },\n {"alias": "ghi_am", "type": "files"},\n {"alias": "nguyen_van", "type": "text", "format": "markdown"},\n {"alias": "hen_lai", "type": "date", "timezone": "Asia/Ho_Chi_Minh"},\n {"alias": "nguoi_thuc_hien", "type": "select_member"}\n ]\n }\n ],\n "records": {\n "khach_tiem_nang": {\n "title": "ho_ten",\n "subtitle": ["ten_cong_ty"],\n "image": "anh",\n "party": "person",\n "status": {"field": "trang_thai", "closed": ["da_chuyen_doi", "loai"], "history": "lich_su_trang_thai"},\n "due": ["han_buoc_tiep"]\n },\n "tin_hieu": {"title": "tom_tat", "subtitle": ["kenh", "ngay_xay_ra"], "image": "anh_chup"},\n "buoc_tiep": {\n "title": "vi_sao_luc_nay",\n "subtitle": ["viec"],\n "mark": "viec",\n "status": {"field": "tinh_trang", "closed": ["xong", "bo_qua"]},\n "due": ["han"]\n },\n "tuong_tac": {"title": "tom_tat", "subtitle": ["qua", "phan_hoi"]}\n },\n "apps": [\n {\n "alias": "khach_tiem_nang",\n "name": "Kh\xE1ch ti\u1EC1m n\u0103ng",\n "description": "M\u1ECDi ng\u01B0\u1EDDi m\xECnh c\xF3 th\u1EC3 b\xE1n d\u1ECBch v\u1EE5, t\u1EEB t\xEDn hi\u1EC7u \u0111\u1EA7u ti\xEAn \u0111\u1EBFn khi chuy\u1EC3n v\xE0o CRM: m\u1EDF m\u1ED9t kh\xE1ch \u0111\u1EC3 \u0111\u1ECDc v\xEC sao n\xEAn g\u1ECDi l\xFAc n\xE0y, li\xEAn h\u1EC7 ngay, ghi l\u1EA1i l\u1EA7n ch\u1EA1m, x\u1EBFp b\u01B0\u1EDBc ti\u1EBFp theo, v\xE0 chuy\u1EC3n \u0111\u1ED5i khi \u0111\xE3 c\xF3 cu\u1ED9c tr\xF2 chuy\u1EC7n.",\n "icon": "radar",\n "theme": {"color": "violet"},\n "entity": "khach_tiem_nang",\n "register": {\n "columns": ["phu_trach", "han_buoc_tiep", "tin_hieu_moi"],\n "filters": ["phu_trach", "tin_hieu_moi"],\n "create": ["ho_ten", "trang_ca_nhan", "so_dien_thoai", "ten_cong_ty", "nguon"],\n "readings": [\n {"breakdown": "khach_tiem_nang", "by": "phu_trach"},\n {"trend": "khach_tiem_nang", "over": "ngay_chuyen_doi", "label": "Kh\xE1ch chuy\u1EC3n \u0111\u1ED5i"}\n ]\n },\n "record": {\n "sections": [\n {\n "title": "\u0110\xE1nh gi\xE1",\n "at": ["moi"],\n "fields": ["vai_tro", "loai_hinh", "quy_mo", "vi_sao_du_dieu_kien", "do_nong"],\n "blocks": [\n {\n "rows": "tin_hieu",\n "title": "T\xEDn hi\u1EC7u",\n "columns": ["loai", "noi_dung"],\n "create": ["tom_tat", "anh_chup", "loai", "noi_dung", "kenh", "lien_ket", "ngay_dang"]\n }\n ],\n "acts": ["danh_gia_dat"]\n },\n {\n "title": "Li\xEAn h\u1EC7",\n "at": ["du_dieu_kien"],\n "fields": ["so_dien_thoai", "phu_trach", "trang_ca_nhan", "zalo", "email", "ghi_chu"],\n "blocks": [\n {\n "rows": "buoc_tiep",\n "title": "B\u01B0\u1EDBc ti\u1EBFp theo",\n "columns": ["ban_nhap"],\n "create": ["vi_sao_luc_nay", "viec", "han", "ban_nhap"]\n },\n {\n "timeline": "tuong_tac",\n "title": "T\u01B0\u01A1ng t\xE1c",\n "create": ["tom_tat", "qua", "phan_hoi", "thoi_diem", "hen_lai"]\n }\n ],\n "acts": ["ghi_cuoc_goi", "da_lien_he"]\n },\n {\n "title": "Chuy\u1EC3n \u0111\u1ED5i",\n "description": "Kh\xE1ch \u0111\xE3 tr\u1EA3 l\u1EDDi v\xE0 mu\u1ED1n \u0111i ti\u1EBFp: chuy\u1EC3n \u0111\u1ED5i t\u1EA1o li\xEAn h\u1EC7 v\xE0 c\xF4ng ty trong s\u1ED5 kh\xE1ch h\xE0ng.",\n "at": ["da_lien_he"],\n "fields": ["nguon", "lien_he", "cong_ty"],\n "acts": ["chuyen_doi"]\n },\n {\n "title": "Lo\u1EA1i",\n "description": "Kh\xE1ch kh\xF4ng \u0111i ti\u1EBFp: ghi l\xFD do \u0111\u1EC3 l\u1EA7n sau kh\xF4ng t\xECm l\u1EA1i.",\n "at": ["loai"],\n "fields": ["ly_do_loai"],\n "acts": ["loai"]\n }\n ]\n },\n "acts": [\n {\n "alias": "danh_gia_dat",\n "label": "\u0110\u1EE7 \u0111i\u1EC1u ki\u1EC7n",\n "when": {"trang_thai": ["moi"]},\n "requires": ["vi_sao_du_dieu_kien"],\n "set": {"trang_thai": "du_dieu_kien"}\n },\n {\n "alias": "ghi_cuoc_goi",\n "label": "Ghi \xE2m cu\u1ED9c g\u1ECDi",\n "when": {"trang_thai": ["du_dieu_kien"]},\n "record": {\n "into": "tuong_tac",\n "audio": "ghi_am",\n "transcript": "nguyen_van",\n "at": "thoi_diem",\n "by": "nguoi_thuc_hien"\n },\n "fills": ["tom_tat", "phan_hoi"]\n },\n {\n "alias": "da_lien_he",\n "label": "\u0110\xE1nh d\u1EA5u \u0111\xE3 li\xEAn h\u1EC7",\n "when": {"trang_thai": ["du_dieu_kien"]},\n "set": {"trang_thai": "da_lien_he"}\n },\n {\n "alias": "chuyen_doi",\n "label": "Chuy\u1EC3n \u0111\u1ED5i",\n "when": {"trang_thai": ["da_lien_he"]},\n "workflow": "chuyen_doi_khach",\n "writes": ["lien_he", "cong_ty"],\n "confirm": true\n },\n {\n "alias": "loai",\n "label": "Lo\u1EA1i",\n "when": {"trang_thai": ["moi", "du_dieu_kien", "da_lien_he"]},\n "asks": ["ly_do_loai"],\n "set": {"trang_thai": "loai"},\n "danger": true\n },\n {\n "alias": "lam_xong",\n "label": "\u0110\xE3 l\xE0m",\n "of": "buoc_tiep",\n "when": {"tinh_trang": ["cho"]},\n "set": {"tinh_trang": "xong"}\n }\n ],\n "checks": [{"field": "khong_lien_he", "resolve": "da_lien_he"}]\n }\n ]\n}\n```\n\n## Approval\n\n**The job.** Raise a payment request, see it through approval, and read what is waiting at the top of the list.\n\n**Read.** `records.de_nghi.status` with its `history` (each move: when and who), sections staged by `at`, and `register.readings` leading with the headline number.\n\n**Keys.** `model/status`, `model/record-page`, `model/readings`\n\n**Not when.** Rows that never move \u2014 a reference list (a price list, a catalog) has no status and no headline number.\n\n```json\n{\n "entities": [\n {\n "alias": "de_nghi",\n "fields": [\n {"alias": "noi_dung", "type": "text"},\n {"alias": "so_de_nghi", "type": "text"},\n {\n "alias": "loai",\n "type": "select",\n "options": [\n {"alias": "thanh_toan_ncc", "color": "violet"},\n {"alias": "nhan_cong", "color": "indigo"},\n {"alias": "chi_phi_cong_truong", "color": "sky"},\n {"alias": "tam_ung", "color": "amber"},\n {"alias": "hoan_ung", "color": "teal"}\n ]\n },\n {"alias": "du_an", "type": "select_record_link", "target_entity": "du_an", "cardinality": "one"},\n {\n "alias": "nha_cung_cap",\n "type": "select_record_link",\n "target_entity": "nha_cung_cap",\n "cardinality": "one"\n },\n {"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {"alias": "han_thanh_toan", "type": "date"},\n {\n "alias": "trang_thai",\n "type": "select",\n "options": [\n {"alias": "nhap", "color": "slate"},\n {"alias": "cho_duyet", "color": "amber"},\n {"alias": "cho_chu_tich", "color": "violet"},\n {"alias": "da_duyet", "color": "blue"},\n {"alias": "da_chi", "color": "green"},\n {"alias": "tu_choi", "color": "rose"}\n ]\n },\n {"alias": "nguoi_de_nghi", "type": "select_member"},\n {"alias": "chung_tu", "type": "files"},\n {"alias": "nguoi_duyet", "type": "select_member"},\n {"alias": "duyet_luc", "type": "date", "format": "datetime"},\n {"alias": "ly_do_tu_choi", "type": "text"},\n {"alias": "phieu_chi", "type": "files"},\n {"alias": "ngan_sach_du_an", "type": "lookup"},\n {"alias": "da_duyet_du_an", "type": "lookup"},\n {"alias": "vuot_ngan_sach", "type": "formula"},\n {"alias": "sat_ngan_sach", "type": "formula"},\n {"alias": "da_gui", "type": "formula"}\n ]\n }\n ],\n "records": {\n "de_nghi": {\n "title": "noi_dung",\n "subtitle": ["du_an"],\n "status": {\n "field": "trang_thai",\n "closed": ["da_chi", "tu_choi"],\n "history": "lich_su_duyet",\n "stages": {\n "nhap": {"owner": "nguoi_de_nghi"},\n "cho_duyet": {"owner": {"role": "giam_doc"}, "within": 1},\n "cho_chu_tich": {"owner": {"role": "chu_tich"}, "within": 2},\n "da_duyet": {"owner": {"role": "ke_toan"}, "within": 2}\n }\n },\n "figure": "so_tien",\n "limits": {"da_duyet_du_an": "ngan_sach_du_an"},\n "due": ["han_thanh_toan"],\n "starts": {"nguoi_de_nghi": "me"}\n }\n },\n "apps": [\n {\n "alias": "de_nghi_chi",\n "name": "\u0110\u1EC1 ngh\u1ECB thanh to\xE1n",\n "description": "Ch\u1EC9 huy tr\u01B0\u1EDFng l\u1EADp \u0111\u1EC1 ngh\u1ECB chi cho d\u1EF1 \xE1n c\u1EE7a m\xECnh, k\xE8m ch\u1EE9ng t\u1EEB, r\u1ED3i g\u1EEDi gi\xE1m \u0111\u1ED1c duy\u1EC7t.",\n "icon": "hand-coins",\n "theme": {"color": "amber"},\n "entity": "de_nghi",\n "register": {\n "columns": ["nguoi_de_nghi"],\n "filters": ["nguoi_de_nghi", "du_an"],\n "group": "du_an",\n "create": ["noi_dung", "loai", "du_an", "nha_cung_cap", "so_tien", "han_thanh_toan", "nguoi_de_nghi"],\n "readings": [\n {\n "metric": "de_nghi",\n "value": "so_tien",\n "where": {"trang_thai": ["da_duyet"]},\n "label": "\u0110\xE3 duy\u1EC7t, ch\u1EDD chi"\n },\n {"trend": "de_nghi", "over": "han_thanh_toan", "value": "so_tien"}\n ]\n },\n "record": {\n "door": "drawer",\n "sections": [\n {\n "title": "\u0110\u1EC1 ngh\u1ECB",\n "at": ["nhap"],\n "fields": ["so_de_nghi", "loai", "nha_cung_cap", "han_thanh_toan", "nguoi_de_nghi"],\n "blocks": [{"files": ["chung_tu"], "title": "Ch\u1EE9ng t\u1EEB k\xE8m"}],\n "acts": ["gui_duyet"]\n },\n {\n "title": "Duy\u1EC7t",\n "at": ["cho_duyet", "cho_chu_tich"],\n "fields": ["nguoi_duyet", "duyet_luc", "ly_do_tu_choi"],\n "acts": ["rut_lai"]\n },\n {"title": "\u0110\xE3 chi", "at": ["da_chi"], "blocks": [{"files": ["phieu_chi"], "title": "Phi\u1EBFu chi"}]}\n ]\n },\n "acts": [\n {\n "alias": "gui_duyet",\n "label": "G\u1EEDi duy\u1EC7t",\n "when": {"trang_thai": ["nhap"]},\n "requires": ["chung_tu", "nguoi_de_nghi"],\n "set": {"trang_thai": "cho_duyet"}\n },\n {\n "alias": "rut_lai",\n "label": "R\xFAt l\u1EA1i",\n "when": {"trang_thai": ["cho_duyet", "cho_chu_tich"]},\n "set": {"trang_thai": "nhap"},\n "confirm": true\n }\n ],\n "checks": [{"field": "da_gui", "blocks": ["edit"]}, {"field": "vuot_ngan_sach"}, {"field": "sat_ngan_sach"}]\n }\n ]\n}\n```\n\n## Dashboard\n\n**The job.** The owner\'s question \u2014 what came in, what went out, where it went, and what is still unmatched \u2014 answered over one period.\n\n**Read.** `dashboard`: `metric`s with `over` (the period windows them), a `trend`, `breakdown`s, and a `list` of rows to act on opening in their app.\n\n**Keys.** `model/dashboards`, `model/readings`\n\n**Not when.** One job\'s headline numbers \u2014 those lead that job\'s register as its `readings`.\n\n```json\n{\n "entities": [\n {\n "alias": "giao_dich",\n "fields": [\n {"alias": "noi_dung", "type": "text"},\n {"alias": "ngay", "type": "date"},\n {\n "alias": "loai",\n "type": "select",\n "options": [{"alias": "thu", "color": "green"}, {"alias": "chi", "color": "rose"}]\n },\n {"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},\n {"alias": "bien_dong", "type": "formula"},\n {\n "alias": "doi_tuong",\n "type": "select_record_link",\n "target_entity": "doi_tuong",\n "cardinality": "one"\n },\n {\n "alias": "danh_muc",\n "type": "select",\n "options": [\n {"alias": "doanh_thu", "color": "green"},\n {"alias": "hang_hoa", "color": "blue"},\n {"alias": "van_chuyen", "color": "indigo"},\n {"alias": "nhan_su", "color": "violet"},\n {"alias": "van_phong", "color": "sky"},\n {"alias": "thue_phi", "color": "slate"},\n {"alias": "ngan_hang", "color": "teal"},\n {"alias": "khac", "color": "gray"}\n ]\n },\n {"alias": "sao_ke", "type": "select_record_link", "target_entity": "sao_ke", "cardinality": "one"},\n {\n "alias": "trang_thai",\n "type": "select",\n "options": [\n {"alias": "chua_khop", "color": "amber"},\n {"alias": "da_khop", "color": "green"},\n {"alias": "khong_can_hd", "color": "slate"}\n ]\n }\n ]\n }\n ],\n "records": {\n "giao_dich": {\n "title": "noi_dung",\n "subtitle": ["ngay"],\n "status": {"field": "trang_thai", "closed": ["da_khop", "khong_can_hd"], "history": "lich_su_khop"},\n "figure": "bien_dong"\n }\n },\n "apps": [\n {\n "alias": "dong_tien",\n "name": "D\xF2ng ti\u1EC1n",\n "description": "Gi\xE1m \u0111\u1ED1c xem trong k\u1EF3: ti\u1EC1n v\xE0o, ti\u1EC1n ra, d\xF2ng ti\u1EC1n r\xF2ng qua c\xE1c k\u1EF3, chi v\xE0o \u0111\xE2u, thu t\u1EEB ai, v\xE0 nh\u1EEFng d\xF2ng sao k\xEA c\xF2n ch\u01B0a kh\u1EDBp.",\n "icon": "chart-line",\n "theme": {"color": "emerald"},\n "dashboard": [\n {\n "metric": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "over": "ngay",\n "label": "Ti\u1EC1n v\xE0o"\n },\n {\n "metric": "giao_dich",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "over": "ngay",\n "better": "down",\n "label": "Ti\u1EC1n ra"\n },\n {"trend": "giao_dich", "over": "ngay", "value": "bien_dong", "label": "D\xF2ng ti\u1EC1n r\xF2ng"},\n {\n "breakdown": "giao_dich",\n "by": "danh_muc",\n "value": "so_tien",\n "where": {"loai": ["chi"]},\n "over": "ngay",\n "label": "Chi theo danh m\u1EE5c"\n },\n {\n "breakdown": "giao_dich",\n "by": "doi_tuong",\n "value": "so_tien",\n "where": {"loai": ["thu"]},\n "over": "ngay",\n "label": "Thu theo kh\xE1ch h\xE0ng"\n },\n {\n "list": "giao_dich",\n "where": {"trang_thai": ["chua_khop"]},\n "columns": ["sao_ke"],\n "app": "doi_chieu",\n "label": "D\xF2ng ch\u01B0a kh\u1EDBp"\n }\n ]\n }\n ]\n}\n```\n';
|
|
@@ -44379,7 +44540,7 @@ var examples_default = '# Worked examples\n\nEach section is one treatment, cut
|
|
|
44379
44540
|
var AGENTS_default = "# @lotics/cli \u2014 agent index\n\nThe model an agent needs before driving this CLI: which surface answers which question, what the\nconventions are, and where the traps are.\n\n| Read | For |\n|---|---|\n| `lotics --help` | The verb inventory (\xA7 COMMANDS) and global flags. The verb LIST is generated and never stale; the prose beside each verb is hand-written, so where it disagrees with `docs/cli_reference.md`, the reference wins. |\n| `lotics tools` \xB7 `lotics tools <name>` | The agent tool registry and one tool's full JSON Schema. |\n| `lotics docs` \xB7 `lotics docs <area>[/<section>]` \xB7 `lotics docs [<area>] --grep <text>` | This CLI's references, carried inside it so each describes the binary answering. Capped at a page: a doc that does not fit hands back its opening and the addresses into it, so the next call is smaller than the last. A page the copy lacks \u2014 one the server added since this binary was built, which a refusal can cite \u2014 is read from the server's `docs` tool with this machine's credential, and so is every `--grep`, over the references the server serves. A custom-code app's SDK reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the app. |\n| `lotics docs model` \xB7 `lotics docs model/<section>` | How to write a `model.json` \u2014 the file a workspace is built from: its tables, how a row of each is recognised (`records`), and the apps stated over them. Every top-level key, every field type with the config it needs, the row format, the rules, and a worked example; complete example models of several trades are listed at `https://lotics.ai/presets/index.json`. `lotics model apply` checks the file (every problem in one run), applies its tables and mints a version of each app; the WORKSPACE remembers what each alias became, so every later apply binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics model pull` goes the other way \u2014 the workspace's model, rebuilt from what owns each part. |\n| `lotics docs design` \xB7 `lotics docs design/<section>` | Designing an app \u2014 where an app starts: the method, the treatment each kind of row takes with the `model` page stating its keys, the visual bar, and looking at what an apply built. A finding of the apply names its section. |\n| `lotics docs examples` \xB7 `lotics docs examples/<treatment>` | Each treatment worked through in an app of a complete model: the job, the keys to read, where it is the wrong one, and the excerpt. |\n| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE \u2014 clarify, model, apply or build, prove, look \u2014 for an app stated in a model and for a custom-code app. Read it once before starting an app. Looking is `lotics run screenshot_app '<json>' -o <dir>`, which saves each PNG and its text snapshot there. |\n| [docs/cli_reference.md](./docs/cli_reference.md) | Per-command contracts, flags, exit codes, and gotchas \u2014 the detail `--help` compresses. |\n| [docs/data_model.md](./docs/data_model.md) | Tables, their fields, and how they relate \u2014 one fact per column, one entity per table and the NAME-OVERLAP probe that says when a split has broken, one vocabulary wherever values are copied, a copy boundary that accounts for every source field, provenance as a link, a declared natural key, history as rows, and why derived DEPTH costs more than row count; then every field type, its properties, computed fields, and what `update_fields` takes. Separate from building_an_app because every workspace starts with tables and many never get an app. |\n| [docs/filters.md](./docs/filters.md) | The one filter grammar every filter-taking tool, view and rollup reads \u2014 conditions, groups, and the operators per field type. |\n| [docs/field_values.md](./docs/field_values.md) | The value each field type takes in a write. |\n| [docs/workflows.md](./docs/workflows.md) | Writing a workflow body \u2014 triggers, steps, what an expression reads, the helpers, approvals and agent steps, and table lifecycle workflows. |\n| [docs/app_bindings.md](./docs/app_bindings.md) | The queries, workflows and agents an app binds \u2014 the query tree, typed inputs and outputs, and an agent's declaration. |\n| [docs/document_templates.md](./docs/document_templates.md) | Generating PDF/Excel/Word/email from reusable templates. |\n| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the workspace facts an agent can't guess; who can read a doc; catalog-then-stage retrieval. |\n| [docs/migration.md](./docs/migration.md) | What to DO when something an earlier CLI wrote to disk no longer matches what it does \u2014 local app projects are gone, and each verb that read one has a replacement. |\n| [README.md](./README.md) | Install, auth, and worked examples. |\n\n## Two surfaces, and the trap between them\n\n**`lotics tools` is not the CLI's capability list.** There are two disjoint surfaces:\n\n- **Tools** (`lotics tools`, `lotics run <tool>`) \u2014 the *agent tool registry*: what an agent, workflow,\n or automation may call. Workspace data, templates, knowledge, admin.\n- **Commands** (`lotics --help` \xA7 COMMANDS) \u2014 the CLI's *own verbs*: auth and org/workspace scoping,\n file upload and download, and those that touch a local file for an app \u2014 `model apply`,\n `model check`, `model pull`, `app create --custom`, `app pull` and `app deploy`.\n\nSeveral capabilities exist **only** as commands and appear nowhere in `lotics tools` \u2014 downloading a\nfile is the one most often mistaken for missing. Concluding \"the platform can't do X\" from the tool\nlist alone is a mistake; check both.\n\n**`lotics <verb> --help` prints that verb's entries from \xA7 COMMANDS** (`lotics model --help`,\n`lotics file download --help`); `lotics report --help` prints the report frame. To find out whether\nsomething exists, read `lotics --help` \xA7 COMMANDS \u2014 the whole section, not a narrow grep.\n\n## Conventions that hold across every command\n\n- **Scope is resolved per invocation.** `LOTICS_ORG` / `LOTICS_WORKSPACE` (or `LOTICS_API_KEY`) scope a\n single call without changing the active org or a directory pin \u2014 the safe way to touch one tenant\n from a shell serving many. Every command that resolves a workspace echoes its target to **stderr**\n (`lotics \u2192 <org> / <workspace>`); read it back before trusting a write. Resolution precedence is\n in README \xA7 Organizations.\n- **A machine with no key can still sign in, and the sign-in never blocks you.** `lotics auth\n login <email>` prints the page a person opens (also mailed) and the code that page must show,\n records the request, and EXITS. They press Confirm whenever they get to it; the next command that\n NEEDS a credential collects the key before doing its own work, so \"sign in\" costs you one command\n and then re-running what you wanted. Never wrap it in a timeout waiting for a human \u2014 `--wait`\n exists if you really want one blocking command, and killing that one is safe too (the request\n survives and the next command claims it). A command run before Confirm exits 1 naming the page and\n code again; once the 15 minutes are up it says to ask again. `lotics setup` falls into the same\n flow by itself when the email it was given already has an account: it stops having created\n nothing, and the SAME command run again carries on.\n- **A credential is either a SIGN-IN or an API KEY, and `logout` treats them differently.** A profile\n from `auth login` / `auth signup` acts as the person who confirmed it and is theirs \u2014 `lotics auth\n logout` revokes it server-side, and it lapses on its own after 90 idle days (each use pushes that\n out). A profile from `auth api-key` holds a key an ADMIN issued. A key created in Settings carries\n its OWN access \u2014 every app and table, or only the ones chosen on the key, so a listing that comes\n back short is the key's reach, not a bug \u2014 while a key created FOR a person carries that\n person's access and dies with their membership. Either is routinely also on a server and on\n other machines, so logout only forgets it locally and says so; only an admin revokes it. A profile that states no kind (saved before the field existed) is resolved against the SERVER\n and revoked only if the answer is a sign-in; a bare `--api-key` / `LOTICS_API_KEY` names no\n profile to remove at all. Nothing is ever revoked on a guess \u2014 between two, the destructive one is\n wrong. `lotics auth whoami` prints the kind, asking the server when the store cannot say.\n- **A key created in Settings never administers the organization, whatever its access.** The verbs\n `docs/cli_reference.md` marks *admin only* split in two under a key: `workspace doctor`, which only\n reads inside a workspace the key reaches, runs as before, while applying or pulling a model\n (`model apply`, `model pull`, `setup`), managing people, sharing or\n ownership, creating or deleting a workspace, changing workspace settings, setting credit limits,\n reading the access log and publishing an app's API answer `403` and name the remedy: an admin signed in, so\n `lotics auth login <email>` and run it again. A sign-in acts as that person and is refused none of\n them. Do not retry a `403` with the same credential and do not ask for a wider key \u2014 no answer on\n the key's own screen grants this.\n- **A 401 names its remedy \u2014 act on the hint, do not retry.** \"This credential expired / was\n revoked / belongs to a member who is no longer active\" carries the one remedy that ends this\n credential: run `lotics auth login <email>` for a sign-in, or ask the admin who issued it for a\n new key. A credential minted before that was recorded carries BOTH, because nothing on the row\n tells them apart \u2014 so on a box with no browser, take the second. Only an UNRECOGNIZED key gets\n the generic \"Invalid or disabled API key\", and that one is generic on purpose, so re-sending it\n teaches nothing. A `reason` rides on the body for a script to branch on, since the code stays\n `unauthorized` for every 401.\n- **Large payloads bypass `ARG_MAX`** \u2014 `lotics run <tool> @args.json`, or piped stdin behind `-`. A leading `@` is\n unambiguously a file path (JSON args start with `{`).\n- **stdout is the payload, stderr is the narration.** Progress, status lines, and the target echo go to\n stderr; the result goes to stdout, so piping stays clean. `--json` switches stdout from the\n agent-readable summary to the full structured object.\n- **Every tool is invoked one way \u2014 `lotics run <tool>`.** Including the ones that RUN something\n (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app\n (`set_app_queries`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command\n exists only for work that touches a local file.\n- **Every change to an app mints a version of it, and rolling back is the undo.** An apply, a\n deploy, a single `set_app_*` or `remove_app_binding`: each is a new version, and `rollback_app` makes an\n earlier one current again. It restores the app \u2014 never a table change or a row a workflow wrote,\n so try a write on a throwaway record.\n- **Exit codes are assertable, and they report the WORK rather than the call.** `lotics run` exits\n non-zero when a `run_app_workflow` or `run_app_agent` result's own envelope carries a failed\n `status` (`error`/`failed`/`cancelled`) \u2014 any other tool's top-level status is data and exits 0 \u2014\n so `lotics run \u2026 && next-step` cannot walk past a refused run; `workspace doctor` exits non-zero on\n findings. An unrecognized status exits 0 \u2014 the list is an allowlist of failure, so a status added\n later never turns a working script red \u2014 and a parked run (`awaiting_input`) is not a failure.\n- **An app that PUBLISHES an API turns every later binding write into a release.** Publishing\n snapshots what the app's queries, workflows and agents promise to callers outside it \u2014 a\n customer's own site or server, which nobody here can redeploy. From then on an additive change\n re-snapshots silently and a breaking one is REFUSED, naming each change; the tool's\n `acknowledge_breaking_api_change` carries it out and snapshots the break as a new contract version.\n- **Exposure is per app, all or nothing** \u2014 a public share or a key reaches every alias an app\n declares, so what outsiders may call is a second app over the same tables\n ([docs/building_an_app.md](./docs/building_an_app.md) \xA7 7).\n- **`--print-created` / `--cleanup` on any call that reports `side_effects`.** The first prints the\n records created plus a paste-ready cleanup plan and what cannot be auto-undone; the second runs\n those deletes (records only \u2014 never files, integrations or notifications). Neither is a rollback.\n- **Text output is the default and is built for reading**; reach for `--json` only when a field is\n needed programmatically.\n- **`lotics report '<json>'` is the channel for what nothing else records.** Reach for it\n when the platform is genuinely missing something (a capability that does not exist \u2014 no\n command ran), when a success was wrong (exited 0, wrong effect), or when an error did\n not name the remedy \u2014 not for your own mistakes, which the logs already show.\n **It is a frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, because a log\n reconstructs what you RAN and never what you WANTED, and that gap is the report. `goal` and\n `actual` are required; there is no severity or category to pick. The\n session's commands attach themselves \u2014 do not retype them. Run it bare for the full prompt; a\n long one rides `@file.json` or an explicit `-` for stdin (bare NEVER reads stdin).\n- **`LOTICS_TELEMETRY=1` correlates a whole session** so the authoring loop's rough edges can be\n found and fixed. Off by default; set it in the shell profile, not per command (each invocation is\n its own process). It sends no arguments, no file contents, and no record data \u2014 see README\n \xA7 Diagnostics.\n\n## Where this CLI is not the answer\n\n- **Editing an app in a local directory.** An app stated in a model is changed by applying the\n model again; its bindings through their `set_app_*` tools; a custom-code app's code by deploying\n its directory again. Nothing reads a local copy of a live app back.\n- **OAuth connections.** Attaching a connected account is web-only; the CLI can list them.\n";
|
|
44380
44541
|
|
|
44381
44542
|
// docs/building_an_app.md
|
|
44382
|
-
var building_an_app_default = '# Building an app, end to end\n\nThe other references here describe **contracts** \u2014 what a model may state, what a tool takes. This\none describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then\nreach for the area doc (`lotics docs`) whenever you need the detail.\n\n**The live app is the only edit surface.** Every change to an app \u2014 applying a model, deploying a\nbuild, setting one query or workflow \u2014 mints a new version of it, and rolling back to an earlier\nversion is the undo. Nothing about an app lives in a local directory the platform reads back.\n\n**Rolling back restores the app, not the data.** A table change the model made, and every row a\nworkflow wrote while you tried it, stay where they are. Try a write on a throwaway record.\n\n---\n\n## 1 \u2014 Two kinds of app\n\n- **An app stated in a model** \u2014 the default, and the right one for almost every job. `model.json`\n says how a row of each entity is recognised (`records`) and, in `apps`, one register over one\n entity and the record its rows open: its columns and filters, its sections in the order its work\n reaches them, its acts and the checks that guard them (`lotics docs model/register`,\n `model/record-page`, `model/acts`, `model/checks`). The platform\n compiles each app and draws it with the runtime every such app shares, so how each piece looks is\n the platform\'s, one way per concept, and no key changes it. Every write the app makes is a\n generated workflow that re-checks on the server what the model states.\n- **A custom-code app** \u2014 React you write, for a surface the model\'s vocabulary cannot state. It\n reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever\n React you choose.\n\n## 2 \u2014 Clarify what is being asked, before modelling it\n\n**A metric name is not a definition.** "Revenue", "in stock", "active", "overdue" \u2014 each is a\nbusiness rule the person asking owns, and the cost of guessing is a screen that is confidently\nwrong. Ask until there is no ambiguity left:\n\n- Which rows count, keyed off which field \u2014 a date, a status, a flag?\n- Does the same metric need a **different rule per table**? One table may key off a date and\n another off a status; one rule rarely covers both.\n- Snapshot or flow? "Current stock" is as-of-now; "revenue this month" is a window. They compile\n to different filters.\n- If the data cannot support the definition asked for \u2014 the field simply is not there \u2014 **say so\n and show the options.** Silently substituting a near-miss produces a number nobody can trace.\n\nThis is the step that gets skipped under time pressure, and it is the only one whose mistakes are\ninvisible in review: every later artifact is correct with respect to the wrong definition.\n\n## 3 \u2014 The data model, before any app\n\nGet this wrong and nothing above it can be precise. Each entity is its own table with links into\nthe spine; attributes and evidence are fields on their owner. A single table with a `type` column\nstanding in for three entities collapses the distinctions every later query needs.\n\n**Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and\nlook at fill rates \u2014 a field that is present and empty on 90% of rows will not support the screen\nyou are about to design.\n\n**That includes imagery.** If the entity has a likeness \u2014 a product, a property, a vehicle, a\nperson \u2014 its picture is the strongest identifier a register row can carry, and an empty image field\nis a data gap to fill before you design around it: `lotics file upload`, then `update_records`.\n\n**One fact, one column \u2014 and check before you add one.** Read the table\'s existing fields before\nadding any, because the fact is often already there in another shape: a place written as text\nbeside a link to the place record, a status word beside the select that decides it, a total beside\nthe formula that computes it. Two columns for one fact never stay equal. Prefer the link, the\nselect, or the formula, and compose the text when you READ.\n\n**Match on ids and option keys, never on rendered text.** Resolve a name to its `rec_\u2026` or `opt_\u2026`\nonce, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.\n\n**How the tables RELATE is `lotics docs data_model`** \u2014 read it before designing a schema; those\ndecisions outlive any one app.\n\n**Empty is not the same as redundant.** A field nothing fills may still be the only home for a real\ndistinction. Read what a field MEANS before you remove it.\n\n## 4 \u2014 An app stated in a model\n\n```\nlotics docs model # how to write model.json, with a worked example\nlotics docs design # how to design each app: the method, a treatment per kind of row\nlotics model apply model.json # check it, apply the tables, mint a version of every app\nlotics model apply model.json --app orders # only the apps named; the tables are applied whole\nlotics model check model.json # what the apply would change, writing nothing\nlotics model pull -o model.json # the workspace\'s model, as the file apply reads\n```\n\n`model apply` checks the whole file first \u2014 every problem in one run, before anything is uploaded\nor written. It then adopts or creates each table (the table this workspace bound it to, else one of the same\nlabel, is adopted and given what it lacks; no stored value changes), writes the first rows only where every bound\ntable is empty, and mints one version per app, printing each app\'s id, the version minted\n(`unchanged` when there was nothing to mint) and its address. Applying the same file again mints nothing.\nA table change is not undone by rolling an app back, so `lotics model check` first says what the apply would\ncreate or change in the tables and which apps it would create, update or refuse \u2014 writing nothing.\n\n**An app leaving out a treatment its rows call for is refused** \u2014 readings over its rows, a record\'s\nsections, an act per status move, a picture where its rows hold photos (`lotics docs design`). Each\nrefusal names the rule, and beside them comes each refused app\'s patch adopting its decisions: merge\nthe patches into the file in the order given, or state why the app stays as it is under the\n`declines` key the refusal names (`lotics docs model/declines`), then apply again. `lotics model check` reads the\ndecisions beside the workspace\'s rows, and with `--json` gives each app as the patches leave it, its\n`draft`.\n\n**The model changes after the app exists, and applying it again is how it lands.** Edit the file,\napply it. An act whose write the model cannot say names its own `workflow`; that workflow\'s body is\nthe live one, and `lotics run set_app_workflow` changes it. An act that reads papers runs an agent\napply owns: made, replaced and removed with the act, and an agent of that alias apply did not make\nis refused, never rewritten.\n\n## 5 \u2014 A custom-code app\n\n```\nlotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk\ncd <dir>\n# edit src/App.tsx \u2014 node_modules/@lotics/app-sdk/AGENTS.md is the reference\nnpm run typecheck && npm run lint && npm test\nlotics app deploy -m "<what changed>" # build, upload, a new version live\nlotics app pull <app_id> # the live version\'s source, on a machine without the project or after another deploy\n```\n\nWhat the app reads and writes is bound on the app, never in the project: `lotics run\nset_app_queries` binds named queries, `lotics run set_app_workflow` a workflow body, and each mints a\nversion. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build\nand carries every binding forward unchanged.\n\n**Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column\n(`{ "source": "fld_\u2026", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with\n`is_current_member` **inside the query** \u2014 a member id passed from the client is chosen by the\ncaller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.\n\n**Workflows are the only way an app writes.** Every workflow bound to an app is also the chat\nagent\'s write surface, so its shape is an agent-facing decision: take a list where one job covers\nmany records, say in an optional input\'s `description` what omitting it means, make the write\nsurvive running twice, and gate anything irreversible with `wait_for_approval`.\n\n## 6 \u2014 Prove it, without a screen\n\nStatic checks prove parse, types and names \u2014 they evaluate nothing. Rehearse a write first:\n\n```\nlotics run dry_run_workflow \'{"trigger_type":"app_workflow","trigger_payload":{\u2026},"live_reads":true}\'\n```\n\nIt walks the real step tree and returns the resolved plan plus expression and tool-input errors,\ndispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it\nevery read returns a stub, and every data-gated branch takes the empty path.\n\nThen run it end to end, on a throwaway record:\n\n```\nlotics run run_app_query \'{"app_id":"app_\u2026","alias":"\u2026","params":{\u2026}}\'\nlotics run run_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","inputs":{\u2026}}\'\n# exits non-zero when the run failed, so it is assertable\n```\n\nA workflow is also how an app **produces a document** \u2014 `generate_document` fills a template\nyou registered once (`lotics docs document_templates`).\n\n## 7 \u2014 Look at it, then name it\n\nLook at every app you apply, at a desktop width and at a phone\'s \u2014 every check above reads the\ndefinition, none of them the pixels:\n\n```\nlotics run screenshot_app \'{"app_id":"app_\u2026"}\' -o ./shots\n# one PNG per width saved into ./shots, each with its text snapshot beside it as <name>.txt;\n# "path":"/rec_\u2026" opens a screen inside the app\n```\n\nFrom the CLI each image is a link signed for an hour, and `-o` saves it. Each shot is one screen, as a member sees it; `"screen":2` draws the next screen of\nthe content scrolling inside the app, and so on to its end, and `"full_page":true` grows one image\nto the first five screens \u2014 one or the other. Beside each image comes its `snapshot`, the screen\'s\naccessibility tree as text \u2014 read it to check labels and values without opening the picture \u2014 and,\nwhere a table left columns off at that width, `shed` names them.\n\nThe app is drawn live, as you, read-only: a screen that writes when it opens shows that write\nrefused, and `errors` lists what failed on the page. A version that looks wrong is one\n`lotics run rollback_app` away from the one before.\n\nFor an app a model applied, `verdict` comes beside the images: what a check of the workspace\'s\nmodel finds of that app now, beside its rows \u2014 what the next apply would refuse it for and the notes\non it, the `patch` adopting the decisions among them, and the app\'s body as the patch\nleaves it (`draft`). `verdict_unread` says why none was read.\n\nThen set the icon, the colour and the app\'s own `description` through `lotics run update_app`. The\n`description` heads the capability listing the member\'s chat agent reads on **every** turn, so a\nstanding process the app expects that agent to carry out belongs there and nowhere else.\n\n**A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key\naccess to it, reaches every alias the app declares; no alias can be held back. So whatever\noutsiders may call is a SECOND app over the same tables: only the queries they may read and the\nworkflows they may run. The desk the team works in stays a separate, private app.\n';
|
|
44543
|
+
var building_an_app_default = '# Building an app, end to end\n\nThe other references here describe **contracts** \u2014 what a model may state, what a tool takes. This\none describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then\nreach for the area doc (`lotics docs`) whenever you need the detail.\n\n**The live app is the only edit surface.** Every change to an app \u2014 applying a model, deploying a\nbuild, setting one query or workflow \u2014 mints a new version of it, and rolling back to an earlier\nversion is the undo. Nothing about an app lives in a local directory the platform reads back.\n\n**Rolling back restores the app, not the data.** A table change the model made, and every row a\nworkflow wrote while you tried it, stay where they are. Try a write on a throwaway record.\n\n---\n\n## 1 \u2014 Two kinds of app\n\n- **An app stated in a model** \u2014 the default, and the right one for almost every job. `model.json`\n says how a row of each entity is recognised (`records`) and, in `apps`, one register over one\n entity and the record its rows open: its columns and filters, its sections in the order its work\n reaches them, its acts and the checks that guard them (`lotics docs model/register`,\n `model/record-page`, `model/acts`, `model/checks`). The platform\n compiles each app and draws it with the runtime every such app shares, so how each piece looks is\n the platform\'s, one way per concept, and no key changes it. Every write the app makes is a\n generated workflow that re-checks on the server what the model states.\n- **A custom-code app** \u2014 React you write, for a surface the model\'s vocabulary cannot state. It\n reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever\n React you choose.\n\n## 2 \u2014 Clarify what is being asked, before modelling it\n\n**A metric name is not a definition.** "Revenue", "in stock", "active", "overdue" \u2014 each is a\nbusiness rule the person asking owns, and the cost of guessing is a screen that is confidently\nwrong. Ask until there is no ambiguity left:\n\n- Which rows count, keyed off which field \u2014 a date, a status, a flag?\n- Does the same metric need a **different rule per table**? One table may key off a date and\n another off a status; one rule rarely covers both.\n- Snapshot or flow? "Current stock" is as-of-now; "revenue this month" is a window. They compile\n to different filters.\n- If the data cannot support the definition asked for \u2014 the field simply is not there \u2014 **say so\n and show the options.** Silently substituting a near-miss produces a number nobody can trace.\n\nThis is the step that gets skipped under time pressure, and it is the only one whose mistakes are\ninvisible in review: every later artifact is correct with respect to the wrong definition.\n\n## 3 \u2014 The data model, before any app\n\nGet this wrong and nothing above it can be precise. Each entity is its own table with links into\nthe spine; attributes and evidence are fields on their owner. A single table with a `type` column\nstanding in for three entities collapses the distinctions every later query needs.\n\n**Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and\nlook at fill rates \u2014 a field that is present and empty on 90% of rows will not support the screen\nyou are about to design.\n\n**That includes imagery.** If the entity has a likeness \u2014 a product, a property, a vehicle, a\nperson \u2014 its picture is the strongest identifier a register row can carry, and an empty image field\nis a data gap to fill before you design around it: `lotics file upload`, then `update_records`.\n\n**One fact, one column \u2014 and check before you add one.** Read the table\'s existing fields before\nadding any, because the fact is often already there in another shape: a place written as text\nbeside a link to the place record, a status word beside the select that decides it, a total beside\nthe formula that computes it. Two columns for one fact never stay equal. Prefer the link, the\nselect, or the formula, and compose the text when you READ.\n\n**Match on ids and option keys, never on rendered text.** Resolve a name to its `rec_\u2026` or `opt_\u2026`\nonce, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.\n\n**How the tables RELATE is `lotics docs data_model`** \u2014 read it before designing a schema; those\ndecisions outlive any one app.\n\n**Empty is not the same as redundant.** A field nothing fills may still be the only home for a real\ndistinction. Read what a field MEANS before you remove it.\n\n## 4 \u2014 An app stated in a model\n\n```\nlotics docs model # how to write model.json, with a worked example\nlotics docs design # how to design each app: the method, a treatment per kind of row\nlotics model apply model.json # check it, apply the tables, mint a version of every app\nlotics model apply model.json --app orders # only the apps named; the tables are applied whole\nlotics model check model.json # what the apply would change, writing nothing\nlotics model pull -o model.json # the workspace\'s model, as the file apply reads\n```\n\n`model apply` checks the whole file first \u2014 every problem in one run, before anything is uploaded\nor written. It then adopts or creates each table (the table this workspace bound it to, else one of the same\nlabel, is adopted and given what it lacks; no stored value changes), writes the first rows only where every bound\ntable is empty, and mints one version per app, printing each app\'s id, the version minted\n(`unchanged` when there was nothing to mint) and its address. Applying the same file again mints nothing.\nA table change is not undone by rolling an app back, so `lotics model check` first says what the apply would\ncreate or change in the tables and which apps it would create, update or refuse \u2014 writing nothing.\n\n**An app leaving out a treatment its rows call for is refused** \u2014 readings over its rows, a record\'s\nsections, an act per status move, a picture where its rows hold photos (`lotics docs design`). Each\nrefusal names the rule, and beside them comes each refused app\'s patch adopting its decisions: merge\nthe patches into the file in the order given, or state why the app stays as it is under the\n`declines` key the refusal names (`lotics docs model/declines`), then apply again. `lotics model check` reads the\ndecisions beside the workspace\'s rows, and with `--json` gives each app as the patches leave it, its\n`draft`.\n\n**The model changes after the app exists, and applying it again is how it lands.** Edit the file,\napply it. An act whose write the model cannot say names its own `workflow`; that workflow\'s body is\nthe live one, and `lotics run set_app_workflow` changes it. A job no press starts \u2014 an agent or a\nservice handing the app a file or a list of rows \u2014 is a workflow the app declares in `workflows`\n(`lotics docs model/apps`): the model states what it takes and hands back, `lotics run\nset_app_workflow` sets its body with `inputs` and `outputs` left out, and every apply keeps that\nbody. A workflow set on the app that its model declares nowhere is archived by the next apply. An act that reads papers runs an agent\napply owns: made, replaced and removed with the act, and an agent of that alias apply did not make\nis refused, never rewritten.\n\n## 5 \u2014 A custom-code app\n\n```\nlotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk\ncd <dir>\n# edit src/App.tsx \u2014 node_modules/@lotics/app-sdk/AGENTS.md is the reference\nnpm run typecheck && npm run lint && npm test\nlotics app deploy -m "<what changed>" # build, upload, a new version live\nlotics app pull <app_id> # the live version\'s source, on a machine without the project or after another deploy\n```\n\nWhat the app reads and writes is bound on the app, never in the project: `lotics run\nset_app_queries` binds named queries, `lotics run set_app_workflow` a workflow body, and each mints a\nversion. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build\nand carries every binding forward unchanged.\n\n**Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column\n(`{ "source": "fld_\u2026", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with\n`is_current_member` **inside the query** \u2014 a member id passed from the client is chosen by the\ncaller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.\n\n**Workflows are the only way an app writes.** Every workflow bound to an app is also the chat\nagent\'s write surface, so its shape is an agent-facing decision: take a list where one job covers\nmany records, say in an optional input\'s `description` what omitting it means, make the write\nsurvive running twice, and gate anything irreversible with `wait_for_approval`.\n\n## 6 \u2014 Prove it, without a screen\n\nStatic checks prove parse, types and names \u2014 they evaluate nothing. Rehearse a write first:\n\n```\nlotics run dry_run_workflow \'{"trigger_type":"app_workflow","trigger_payload":{\u2026},"live_reads":true}\'\n```\n\nIt walks the real step tree and returns the resolved plan plus expression and tool-input errors,\ndispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it\nevery read returns a stub, and every data-gated branch takes the empty path.\n\nThen run it end to end, on a throwaway record:\n\n```\nlotics run run_app_query \'{"app_id":"app_\u2026","alias":"\u2026","params":{\u2026}}\'\nlotics run run_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","inputs":{\u2026}}\'\n# exits non-zero when the run failed, so it is assertable\n```\n\nA workflow is also how an app **produces a document** \u2014 `generate_document` fills a template\nyou registered once (`lotics docs document_templates`).\n\n## 7 \u2014 Look at it, then name it\n\nLook at every app you apply, at a desktop width and at a phone\'s \u2014 every check above reads the\ndefinition, none of them the pixels:\n\n```\nlotics run screenshot_app \'{"app_id":"app_\u2026"}\' -o ./shots\n# one PNG per width saved into ./shots, each with its text snapshot beside it as <name>.txt;\n# "path":"/rec_\u2026" opens a screen inside the app\n```\n\nFrom the CLI each image is a link signed for an hour, and `-o` saves it. Each shot is one screen, as a member sees it; `"screen":2` draws the next screen of\nthe content scrolling inside the app, and so on to its end, and `"full_page":true` grows one image\nto the first five screens \u2014 one or the other. Beside each image comes its `snapshot`, the screen\'s\naccessibility tree as text \u2014 read it to check labels and values without opening the picture \u2014 and,\nwhere a table left columns off at that width, `shed` names them.\n\nThe app is drawn live, as you, read-only: a screen that writes when it opens shows that write\nrefused, and `errors` lists what failed on the page. A version that looks wrong is one\n`lotics run rollback_app` away from the one before.\n\nFor an app a model applied, `verdict` comes beside the images: what a check of the workspace\'s\nmodel finds of that app now, beside its rows \u2014 what the next apply would refuse it for and the notes\non it, the `patch` adopting the decisions among them, and the app\'s body as the patch\nleaves it (`draft`). `verdict_unread` says why none was read.\n\nThe app\'s `name`, `icon`, `theme` and `description` are keys of the app in the model, and each apply\nwrites them onto the app; one the model leaves out is cleared. `lotics run update_app` changes them on\nthe app, and `lotics model pull` reads them back as the app holds them. The\n`description` heads the capability listing the member\'s chat agent reads on **every** turn, so a\nstanding process the app expects that agent to carry out belongs there and nowhere else.\n\n**A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key\naccess to it, reaches every alias the app declares; no alias can be held back. So whatever\noutsiders may call is a SECOND app over the same tables: only the queries they may read and the\nworkflows they may run. The desk the team works in stays a separate, private app.\n';
|
|
44383
44544
|
|
|
44384
44545
|
// docs/cli_reference.md
|
|
44385
44546
|
var cli_reference_default = "# @lotics/cli \u2014 CLI Command Reference\n\nPer-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. Start at [AGENTS.md](../AGENTS.md) for the model this reference assumes; `lotics --help` is the authoritative, always-current verb list.\n\n| Command | What it does |\n|---|---|\n| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (\xA7 COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics model --help`, `lotics file download --help`); `lotics report --help` prints the report frame. |\n| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open \u2014 `https://lotics.ai/cli_login/<request_id>`, also mailed \u2014 and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential \u2014 `docs` among them \u2014 claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |\n| `lotics auth api-key [key]` | `whoami` \u2192 **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth web` | Send a magic link email to access the web app (requires auth) |\n| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key \u2014 read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |\n| `lotics auth logout [<name\\|id>]` | In a pinned dir: delete the local pin. Else: remove the profile (default the active org), `--all` for every one. What happens server-side depends on which KIND of credential it is. A **sign-in** (`auth login` / `auth signup`) is revoked \u2014 logging that terminal out ends its credential rather than leaving a live one behind; a server that cannot be reached, or a credential already dead, never blocks the local forget, and one line names the org and Settings \u2192 Security \u2192 *Keys and terminals*. An **API key** (`auth api-key`) is only forgotten here \u2014 an admin issued it and it is routinely on a server and on other machines, so one terminal signing out must not kill it for everyone; the line says it is still active and names both pages, because Settings \u2192 API keys is admin-only and the credential may well be the holder's own sign-in, which they revoke themselves at Settings \u2192 Security \u2192 *Keys and terminals*. A profile saved before the kind was recorded states nothing, so the SERVER is asked (`auth whoami`) and it is revoked only if the answer is a sign-in: an older server, a credential minted before the column, and a request that fails all leave it alone. |\n| \u2014 | **A refused credential says which of three ways it is dead, and names the remedy that ends its kind.** `This credential expired.` / `was revoked.` / `belongs to a member who is no longer active in this organization.` carries `Run \\`lotics auth login <email>\\` to sign in again.` for a sign-in and `Ask an admin for a new API key (Settings \u2192 API keys).` for an issued key. A credential minted before that was recorded still gets BOTH in one sentence, because nothing on the row tells them apart \u2014 so a headless box is never sent looking for a browser alone. A key the server does not recognize at all gets one flat `Invalid or disabled API key.` \u2014 deliberately, so a guessed key learns nothing, not even that it named a row. The body carries `reason` for a script to branch on, since the code stays `unauthorized` for every 401. |\n| `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |\n| `LOTICS_ORG=<name\\|id>` | Scope every command in this shell to one saved org. **Resolved once, before any command dispatches**, so a value matching no saved credential refuses every verb with one sentence \u2014 a read, a write, and a local check that needs no credential alike \u2014 and refuses it before the first byte is written. It refuses even when a credential arrives another way, because `--api-key` / `LOTICS_API_KEY` outrank it in the precedence chain and a write must never fall through to whatever THOSE name while the variable says otherwise; when the variable resolves and a key is also given, the key decides and the command says so. The refusal lists the orgs this machine holds, so it is answerable without another command (`lotics org` is refused by the same rule). A name is whatever the credential was SAVED under \u2014 a server-side rename never moves it, and the new name resolves too, so both keep working and `lotics org` prints the pair. |\n| `lotics org use <name\\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous \u2192 error) or id. No flag \u2192 global `active_org`; `--local` \u2192 a `.lotics/config.json` pointer in the current dir. |\n| `lotics workspace` | List workspaces in the active org, marks current with `(current)` |\n| `lotics workspace select <id>` | Set the workspace in the **active scope** \u2014 a local pin if the dir has one, else the active org's global profile. Records the workspace's NAME beside its id, which is what the `lotics \u2192 <org> / <workspace>` echo prints; `workspace list`, `workspace create`, `workspace rename` and `org use` record it too, so a target is named rather than identified. Until one command has listed it, the echo prints the id and says the name is not known yet. |\n| `lotics workspace create <name> [--timezone <Area/City>] [--currency <ISO>]` | Create a new workspace (admin only), auto-switches to it. Neither flag is defaulted from THIS machine, unlike signup: an extra workspace is routinely created by an operator for somebody else. Without `--timezone` the new workspace inherits the zone of the org's OLDEST workspace; without `--currency` it takes the org's default. Both ride the create, so the workspace is never briefly denominated in a currency nobody asked for. `--currency` takes an ISO-4217 code (case-insensitive; anything else is refused). |\n| `lotics workspace rename <name>` | Rename the **current** workspace (admin only) \u2014 the endpoint takes its target from the request's workspace, never a path id, so switch with `workspace select <id>` first and read the `lotics \u2192 <org> / <workspace>` echo before trusting it \u2014 both halves are names, and the rename moves the cached one in the same act. Carries the workspace's existing `default_currency` and `timezone` through unchanged: the endpoint takes the whole settings triple, so sending only a name would blank the other two. |\n| `lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]` | Change the CURRENT workspace's name, default currency or timezone \u2014 `PATCH /v1/workspace`, admin only. Only what you name changes; the endpoint takes the whole triple, so the CLI carries the two you did not. `rename` is this verb with the name alone, which is why it can never forget the other two. Both values are invisible once they are wrong: the currency decides how every money field RENDERS and the zone decides how every date BUCKETS, on a workspace whose whole purpose may be to look like the customer's own. `--json` prints the updated workspace. |\n| `lotics workspace delete <id> --yes` | Delete a workspace by id (admin only). **Soft delete** \u2014 `archived_at` is set, so it drops out of listings, can no longer be selected, and its tables/records go dark, while the data is retained and recoverable. Its **apps are cascade-archived** too \u2014 every app entry point (embedded, public link, standalone subdomain, incl. anonymous public links) stops serving. Refuses the org's **only** active workspace (400) and any workspace outside the caller's org (404). Requires `--yes` to confirm (destructive; the CLI is used non-interactively). |\n| `lotics workspace doctor` | Report workspace-wide dangling schema references via `GET /v1/workspaces/dangling-references` \u2014 every active app/workflow artifact whose prefixed schema id no longer resolves, printed as `<referent.kind> \"<name>\" (<id>) \u2192 <namespace> <id> (missing)`; healthy prints a one-line all-clear. **Exits non-zero (exit 1) on findings** so scripts can gate on it. Resolves the first workspace like every data command (runs before the global workspace resolution). Admin-only. |\n| `lotics tools` | List tools by category with descriptions |\n| `lotics tools <name>` | Full description, input JSON Schema, and the output schema where the tool answers a shape, for one tool; a workflow step says to call it from a workflow body, not `lotics run` |\n| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \\| lotics run <tool> -`) \u2014 both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone \u2014 the error names both escapes. |\n| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |\n| `lotics run <tool>` \u2014 **file cells** | A file in a tool's result carries its `fil_\u2026` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file \u2014 this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |\n| \u2014 | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app (`set_app_queries`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command exists only for work that touches a local file: `model apply`, `model check`, `model pull`, `app create --custom`, `app pull`, `app deploy`. |\n| \u2014 | **The exit code reports the WORK, not just the call \u2014 for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) exit non-zero and print `<tool> \u2192 <status>: <message>` to stderr, so `lotics run \u2026 && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE \u2014 an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |\n| `lotics run <tool> '<json>' -o <dir>` | Also save every **signed link** the answer carries \u2014 an object with a `url`, a `filename` and the `expires_at` its signature ends at, at any depth, such as `screenshot_app`'s images. Each is fetched into `<dir>` (created if missing) under its `filename`, taking a free spelling beside a file of that name already there, and a link's `snapshot` text is written beside it as `<name>.txt`. The written paths follow the answer on **stdout**, one per line (on stderr under `--json`, whose stdout stays the one object). A link is signed for an hour; one the store no longer serves exits non-zero. A workspace file states no `expires_at` and is left to `lotics file download`. |\n| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |\n| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes \u2014 harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |\n| `lotics file upload <file\\|dir...>` (alias `lotics upload`) \xB7 `--stdin` \xB7 `--base64` \xB7 `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files`, several such files to a request up to half that route's 100 MiB cap; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose \u2014 one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** \u2014 an attachment decoded in memory, a generated document, a signed download link \u2014 each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY \u2014 `Buffer.from(s, \"base64\")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |\n| `lotics file download <file_id> [<path>]` \xB7 `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` \u2192 fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there \u2014 the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once \u2014 a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_key> [-o <dir>]` spreads every file on a record's file field over a DIRECTORY \u2014 there is no single file for N files to be. |\n| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first \u2014 id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset \u2014 pass it back as given \u2014 so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to \"what is in here\" short of reading Postgres. |\n| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. Deleting an original deletes every version of it; a file's current version cannot be deleted on its own, and a document template's file, or a version of it, only by a member who manages the template. **Refused while a record cell, a comment, a knowledge doc, a document template, a voice session or a pending change request still references the file or a version it takes along** \u2014 the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what \"deleted\" means here. There is no `lotics delete`: the verb needs its noun. |\n| `lotics knowledge list [--include-hidden]` | `GET /v1/knowledge_docs` \u2014 a table of id, name, tags, description (`--json` for the docs). **REST, not the `list_knowledge` tool**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. |\n| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \\| --content <str>)` | Read the body client-side (a file XOR an inline string \u2014 exactly one required), then call `create_knowledge` with `{ name, description, content, tags? }` (description defaults to `\"\"`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |\n| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) \u2192 the doc with its **hydrated `content`** (the one content-read path for a non-sandbox client). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |\n| `lotics knowledge update <id> [--from <file.md> \\| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `update_knowledge` with **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set \u2014 the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |\n| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` \u2014 one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |\n| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** \u2014 the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches \u2014 while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as \"everything\". |\n| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |\n| `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes \u2014 `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** \u2014 it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as \u2014 that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created \u2014 the design decisions an apply would refuse it for too (`lotics docs design`), since a new workspace holds no rows to change them \u2014 because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name \u2014 the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning \u2014 `setup` writes nothing to disk \u2014 so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** \u2014 what `lotics model apply --json` does (`tables`; `apps`, each with `alias`, `app_id`, `version_id`, `origin`, `address` and `findings`; and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup \u2026`. |\n| `lotics model apply <model.json> [--app <alias> ...] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked against the model's own rules with the validator the server runs, every problem in one run, before anything is uploaded. **The apply refuses an app leaving out a treatment its rows call for** (`lotics docs design`) \u2014 judged by the server beside the workspace's rows, before it writes anything \u2014 until the app adopts it or states why not under the `declines` key the refusal names; the refusal carries each refused app's patch adopting its decisions, to merge in the order printed. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. **Where the file was pulled (`-o`) or applied in this workspace before, only what it changed since is sent**, as a `patch`: what changed in the workspace since and the file does not touch stays. An item named by its `alias` that the file puts in a new place \u2014 an act among an app's acts, a field between two others \u2014 is sent as a move beside the patch: inside an app it is built into the app, an option of a select takes its new place among that select's options on the table, and a table's fields and views, the roles, the templates and the apps themselves keep the order the workspace holds, said as a note. A list whose items carry no `alias` \u2014 an app's columns, a section's fields, the sections \u2014 is sent whole in its new order. What the file takes out is sent as a removal: it goes where an apply rebuilds it (an app's acts and columns, a write rule) and is refused, naming the tool that deletes it, where an apply never deletes it (a table, a field, an option, an app). A file stating a `null` a patch would read as a removal sends the whole model, said on stderr, and so does a reorder sent to a server that takes no move. A patch builds the apps it states or moves an item of and each other app whose build it changes, `--app` naming others; one whose every key and move lies inside one app's body leaves the tables as they stand \u2014 its `tables` is empty \u2014 and checks and builds that app alone, beside any other app whose build reads what it changed. The copy each change is read against is kept per workspace and file under `~/.lotics/model_bases`: an apply narrowed by `--app` leaves in it the other apps as they were, so the next apply sends their edits again, and an apply that fails \u2014 refused, or cut off \u2014 leaves none, so the next sends the whole model. Then the tool adopts or creates every table (the table this workspace bound the entity to, else an existing table of the same label, is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares, or the patch builds \u2014 `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app it built \u2014 alias, `app_id`, `created`/`updated` with the version minted or `unchanged`, and the address it is served at \u2014 then the model's notes once, as the server states them. **What it would change, writing nothing, is `lotics model check`.** **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` on stdout, or `{ok: false, findings}` when the file does not check. |\n| `lotics model check <model.json> [--app <alias> ...] [--json]` | **What applying the model would change, through the `check_model` tool, writing nothing.** The file is read and checked as `model apply` checks it, then sent the same way \u2014 whole, or as what it changed since its base \u2014 and nothing is uploaded or kept. It prints what the apply would do to the schema (what it would add, each kind counted \u2014 tables, fields, options, constraints \u2014 what it leaves as the workspace has it, what the two disagree about), which apps it would create or update, and what it would refuse an app for \u2014 the decisions among its findings, read beside the workspace's rows, with each refused app's patch \u2014 exiting 1 when it would refuse one; workflow bodies are checked only at apply, since they name fields a check has not created. `check_model` only reads, so a key holding read access alone runs this where `model apply` is refused, and so does a private instance whose licence admits reads only: it needs `schema:read`, and `data:read` when the file states an app or is sent as what it changed. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` \u2014 each table and app what the apply would do, an app the patches change carrying its body as they leave it as `draft` \u2014 and `designs`, each refused app's patch in merge order, or `{ok: false, findings}` when the file does not check. |\n| `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** \u2014 the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version \u2014 through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. What the workspace holds that a model cannot state is printed on stderr, never written into the file. Applying what it wrote changes nothing. With `-o`, what it wrote is the copy the next `model apply` of that file reads its changes against. |\n| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty \u2014 before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`get_app_types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: an app the runtime draws from a model is made by `lotics model apply`. The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |\n| `lotics app pull [app_id] [path]` | **A custom-code app's live source, as a project ready to deploy on it.** The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** \u2014 but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`get_app_types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). A JSON app has no source tree: the server's refusal names `get_model`, and `lotics model pull` is its pull. |\n| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`get_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged \u2014 those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |\n| `lotics docs` \\| `lotics docs <area>[/<section>]` \\| `lotics docs [<area>[/<section>]] --grep <text>` | **This CLI's own references, carried inside the binary** \u2014 the model reference, this index, and every doc under `docs/` \u2014 so the doc a reader opens describes the binary answering, listed by the job a reader comes to do. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. **A reference the binary's copy lacks, or a part of one the server serves, is read from the server's `docs` tool** with this machine's credential, after the copy's refusal \u2014 a page the server added since this binary was built, which a server refusal can cite. A name matching more than one reference, or a part missing from a guide to this CLI, is answered by the copy alone. **`--grep` searches the references the server serves** (not this CLI's own guides, which it refuses), with this machine's credential, narrowed to the area or section named: literal text unless `--regex`, with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` \u2014 the dialect `grep_knowledge` reads \u2014 each hit under the address that opens its page; any of those options without `--grep` is refused. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |\n| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script \u2014 the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe \u2014 a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |\n| `lotics docs model` \\| `lotics docs model/<section>[/\u2026]` | **The model reference, from inside the binary** \u2014 it describes this CLI's own model checker, at this CLI's version; `lotics docs` lists it under \"Build an app\", after `design`. Its first page is what a model composes with, the working order (jobs \u2192 entities and fields \u2192 `records` \u2192 one app per job \u2192 `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `\"<entity-alias>:<ref>\"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **Offline, no account.** |\n| `lotics report '<json>'` \\| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` \u2014 invoking it IS the consent that passive collection needs an opt-in for \u2014 but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** \u2014 a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |\n\n";
|
|
@@ -44394,13 +44555,13 @@ var filters_default = '# Filters \u2014 choosing records by their fields\n\nOne
|
|
|
44394
44555
|
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';
|
|
44395
44556
|
|
|
44396
44557
|
// docs/workflows.md
|
|
44397
|
-
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\nAn automation that brings rows in \u2014 on a schedule, or a webhook an outside system calls \u2014 is marked\nas importing into their table with `imports_into` (a table id) when it is created or edited. Its\nruns are then the imports log of a register over that table that keeps one (`register.imports`);\non the CLI, `get_automation` and `query_automations` read the mark back.\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';
|
|
44558
|
+
var workflows_default = '# Workflows \u2014 the steps a workflow runs\n\nA workflow is written as a strict subset of JavaScript. The source is never run as JavaScript: a\nsave parses it into steps, type-checks it against what its trigger supplies, and stores the steps.\nOnly the forms below are accepted; anything else is refused at save with the line, the column and\nwhat to write instead. A save that succeeds may still return `warnings` \u2014 advisory hints such as a\nloop that may never end. Read them.\n\nThe same grammar serves an automation, a table\'s lifecycle workflow and an app\'s workflow, and the\nexpressions of a table check. Only an automation\'s source opens with a trigger declaration; a\nlifecycle workflow takes its trigger from its table and event, an app workflow from the app that calls\nit.\n\n## Triggers\n\nAn automation answers an event outside the records: a schedule, a webhook, an inbound email. A\nreaction to a record being created, updated or deleted is a table lifecycle workflow (see\n**Table lifecycle workflows**).\n\n```\non({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });\n```\n\nEvery source below is typed, so a misspelled member is refused at save rather than read as nothing.\nAll three carry `trigger.trigger_id`.\n\n| trigger | required config | what the body reads |\n|---|---|---|\n| `recurring_schedule` | `cron_expression` | `runtime.timezone`; the current time is `now()` |\n| `receive_webhook` | `secret`? | `trigger.method`, `trigger.headers[...]`, `trigger.query[...]`, `trigger.body` (any JSON, or raw text when the request is not JSON), `trigger.received_at` |\n| `receive_gmail_email` / `receive_outlook_email` | `connected_account_id`, `filter` | `trigger.email_id`, `trigger.from`, `trigger.to`, `trigger.cc`, `trigger.bcc`, `trigger.subject`, `trigger.date`, `trigger.body` (plain text), `trigger.reply_to`, `trigger.in_reply_to`, `trigger.attachments` (file refs \u2014 assign them straight to a files field). One type serves both providers, so `labels`, `thread_id`, `importance` and `conversation_id` are not readable. |\n\nA whole automation:\n\n```\non({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });\n\nconst open = await query_records({\n table_id: "tbl_tickets",\n filters: { node_type: "condition", field_key: "fld_status", operator: "has_any_of", value: ["opt_open"] },\n});\nawait send_email({\n to: "team@example.com",\n subject: "Weekly digest",\n body: `${size(open.records)} tickets open as of ${formatDate(now(), "yyyy-MM-dd")}.`,\n});\n```\n\n## Steps\n\n| step | syntax |\n|---|---|\n| tool call, kept | `const <id> = await <tool>({ ...inputs });` \u2014 also `let x = await \u2026` and `x = await \u2026` |\n| tool call | `await <tool>({ ...inputs });` \u2014 a connection\'s tools too, each a workflow step (`bkav_submit_invoices`, `gmail_send_email`, \u2026); a step that fails ends the run with its reason unless a `try`/`catch` around it handles it |\n| agent | `const <id> = await agent({ instructions, input, tools, model, output });` \u2014 see **An agent step** |\n| app agent | `const <id> = await app_agent({ alias: "<agent alias>", input: { ... } });` \u2014 runs one of the app\'s declared agents, in an app workflow only, and resolves to its declared outputs or its final text. `wait: false` only starts the run and resolves to `{ run_id }`; the run\'s failure is then its own, not the workflow\'s. Refused in a run an agent started. |\n| bind | `const <id> = <expression>;` \u2014 evaluated once; later steps read `<id>`. Bind any expression used twice. |\n| if / else | `if (<expr>) { ... } else { ... }` \u2014 `else` optional, `else if` chains allowed |\n| switch | `switch (<expr>) { case "X": { ... } default: { ... } }` \u2014 string cases only, no fallthrough |\n| for | `for (const <name> of <expr>) { ... }` |\n| wait | `await wait({ duration_in_minutes: 5 });` |\n| wait for an event | `await wait_for_event({ event_type: "webhook", event_ref: <expr>, timeout_in_minutes: 60 });` |\n| wait for an approval | `await wait_for_approval({ approvers: <expr>, prompt: <expr> });` \u2014 see **Waiting for an approval** |\n| return | `return({ status: "success" \\| "error", message: <expr>, field_errors: { ... }? });` |\n| validate | `validate({ checks: [{ fail_when: <expr>, field_key: "...", message: <expr> }, ...] });` |\n\nEvery step\'s inputs, the values each takes and its answer: `lotics tools <name>` prints them; the steps only a body\nruns \u2014 a connection\'s, a mail\'s \u2014 are all in `workflow_steps`. `query_integration_tools` lists the workspace\'s\nconnections and the steps each opens.\n\n`return` is a call, not a JavaScript `return` statement. `validate` ends the workflow with an error\nwhen a check\'s `fail_when` is truthy; the check\'s `field_key` names the control its message lands on \u2014\nin an app workflow a declared input, or the `fld_` key of a field on a table the body names; a field of\nthe trigger table in a lifecycle workflow; in an automation it is not checked.\n\nThe name in `const x = await tool({...})` is the step\'s id; later expressions read its output as\n`x.records`, `x.id` and so on. A `// id: my_step` comment directly above a statement sets an explicit\nid; any other `//` comment there becomes the step\'s description.\n\n**`await` inside a statement** runs the call as its own step first, then reads its output where it is\nwritten: `size((await query_records({...})).records)`, an `if` test, a `return` value, a `for-of`\niterable, a tool input. `const x = c ? await t({...}) : null;` is stored as the `if` it means. An\n`await` that would run on some paths only \u2014 inside `&&`, `||`, `??`, `?.`, a nested ternary, a loop\ntest, a `validate` check \u2014 is refused; write the `if`.\n\n**Names are block-scoped, as in JavaScript** \u2014 `const`, `let`, loop items and `catch (e)`: sibling\nblocks may reuse a name and an inner block may shadow an outer one. A local may take a helper\'s name\n(`const size = 3;`), but calling `size(...)` while it is in scope is refused. Tool names, the reserved\nroots, `linked` and `_s` followed by digits cannot be bound.\n\n### Waiting for an approval\n\n`wait_for_approval` is the wait worth binding \u2014 `wait` and `wait_for_event` carry nothing to read.\nBind it to branch on the decision:\n\n```\nconst approval = await wait_for_approval({\n approvers: record["fld_approvers"],\n prompt: `Approve the quote for ${record["fld_name"]}?`,\n});\n\nif (approval.status == "approved") {\n // the approved path\n} else {\n // the rejected or timed-out path; approval.decision_comment says why\n}\n```\n\nIt resolves to `{ status: "approved" | "rejected" | "timed_out", decided_by: MemberId | null,\ndecided_at: ISO string, decision_comment: string | null }`.\n\n`approvers` takes a bare member id (`"mbr_\u2026"`), a bare group id (`"grp_\u2026"`) or a full principal\n(`{ type: "member_group", id: "grp_x" }`), alone or in a list \u2014 so a member field\'s value passes as it\nis.\n\n### An agent step\n\n`const x = await agent({ instructions, input, tools, model, output });` runs a model with tools as one\nstep \u2014 summarize a record, classify, draft text.\n\n- `instructions` \u2014 a fixed string.\n- `input` \u2014 an object literal; its fields are expressions, evaluated and handed to the model (`{}` for\n none). It is the ONLY workflow data the agent sees: read `record`, prior steps, `runtime` or the loop\n item here, never inside `instructions` or `tools`, which are fixed literals and read no binding.\n- `tools` \u2014 the names of the tools it may call.\n- `model` \u2014 optional; omit it to follow the platform\'s default model.\n- `output` \u2014 `{ mode: "text" }` resolves to a string; `{ mode: "object", schema }` to a typed object.\n\nOnly `x` comes back: in text mode `x` is the string itself, in object mode `x.field` reads a field the\n`schema` declares. The agent\'s own tool calls are not readable.\n\n## Keys, not names\n\n1. **Fields and options are named by key.** A field reads as `record["fld_status"]`, an option as\n `"opt_done"`, a linked field as `linked(record["fld_customer"])[0]["fld_name"]`. `get_table` lists\n each field\'s `key` and each option\'s `key`; a display name is refused at save with the key to use.\n2. **`==` against a multi-select means "includes".** `record["fld_tags"] == "opt_urgent"` on a\n multi-select is stored as `includes(record["fld_tags"], "opt_urgent")`; either spelling works.\n3. **Text renders labels.** Inside a template or a text tool input, a select, member or link value\n renders its display text: `` `${record["fld_status"]}` `` writes "Done", not `opt_done`.\n\n## What an expression reads\n\n- `record` \u2014 the trigger record (on an update, the record as it now stands).\n- `prev_record` \u2014 the record before the change, same shape (updates and deletes only).\n- `changes` \u2014 the fields an update changed, a PARTIAL map: `changes["fld_x"]?.next_value` and\n `?.prev_value`, the `?.` required (updates only).\n- `trigger` \u2014 what a non-record trigger carries (see **Triggers**). `trigger.data`,\n `trigger.prev_data` and `trigger.changes` are the same three roots spelled long; a saved body reads\n back in the short form.\n- a loop\'s item name, and `index`, the iteration\'s 0-based index.\n- `<step id>` \u2014 an earlier step\'s output (`dup.records`).\n- `runtime.timezone`, `runtime.workflow_id`, `runtime.execution_id`, `runtime.workspace_id`,\n `runtime.organization_id`, `runtime.change_origin`, `runtime.triggered_by_member_id`. The current\n time is `now()`.\n\n## Paths\n\n- `record.fld_x` and `record["fld_x"]` read a field; `[n]` reads a list\'s n-th item.\n- A path may start at an awaited call: `(await get_record({...})).data["fld_x"]`. It may not start at a\n helper\'s result \u2014 `first(found.records).id` is refused; bind `first(found.records)`, then read it.\n- A record reads in its declared shape wherever it comes from \u2014 the trigger, a tool result, a `let`, a\n callback parameter: a single select or member as its key or null, a link as its ids. (The record\n tools called outside a workflow return the stored arrays instead.)\n- `linked(record["fld_link"])[n]["fld_x"]` fetches the n-th linked row and reads a field of it; bare\n `linked(record["fld_link"])` is every linked row, one fetch per row. Its argument is a path ending on\n a link field and carries the guard: `linked(record?.["fld_link"])[0]`.\n- `changes["fld_link"]?.next_value` is the same list of ids; `linked()` over a change is refused \u2014\n read `linked(record["fld_link"])` or `linked(prev_record["fld_link"])`.\n\n## Operators and statements\n\nOperators in JavaScript precedence: `? :`, `||`, `??`, `&&`, `== !=`, `< <= > >=`, `+ -`, `* /`, prefix\n`! -`.\n\n- `??` is `coalesce(left, right)`; `?.` reads through a null (`a?.b`, `a?.[0]`, and `s?.trim()` for the\n method names listed under **Helpers**). `x?.()` is refused: helpers and tools are not values.\n- Templates use backticks and `${...}`.\n- Destructuring \u2014 `const { fld_status, fld_name } = record;` binds each name. Renames\n (`{ fld_status: s }`), quoted keys (`{ "fld_ref": ref }`), array patterns with holes\n (`const [first, , third] = xs;`) and defaults (`{ fld_note = "" }`, which apply on null too) work on\n `const`, `let` and `for (const { id } of rows)`. A tool result destructures directly:\n `const { records } = await query_records({...});`. `const a = 1, b = 2;` declares both. Nested\n patterns and rest are refused.\n- Spread \u2014 `[...a, b]` and `{ ...a, b: 1 }` (later keys win). Refused inside a tool input: bind the\n merged value first and pass the binding.\n- Shorthand \u2014 `{ table_id }` is `{ table_id: table_id }`.\n- `xs.push(a, b);` appends, also on a key (`o.items.push(v)`); `o.a.b = v;` sets a nested key;\n `x ??= v`, `x ||= v` and `x &&= v` assign. A `const` may be pushed to or have a key set, as in\n JavaScript; a loop item is read-only.\n- `undefined` is the same value as `null`. `=== undefined` and `!== undefined` are refused, since they\n cannot tell a missing key from null: test `isNull(x)`, or `includes(keys(o), "k")` for whether a key\n was sent.\n- A filter node may leave out `node_type` when its keys say which it is: `field_key` / `operator` /\n `value` a condition, `logic` / `children` a group, `path` / `condition` a traversal.\n- `function name(p = <default>) { return <expr>; }` \u2014 top level only, expanded at every call (a call\n may come before it). Every parameter needs a default, which gives it its type; the body is one\n `return <expr>;` reading only its parameters, helpers and the reserved roots \u2014 never the caller\'s\n names, nor `index`. No recursion; a function never called is refused. A body read back shows the\n expression at each call, not the `function`.\n\n**try / catch.** `try { ... } catch (e) { ... }` catches a tool error or an expression error inside the\nbody; `e` is `{ message, type, step_id?, detail? }`. A failed `validate` and a `return` are not\nerrors \u2014 they end the workflow \u2014 and a step that runs after a wait inside the `try` is outside it.\n`finally` is refused: put always-run steps after the `try`.\n\n**Loops.** `for-of`, `while (cond) { ... }`, `do { ... } while (cond);` and\n`for (let i = 0; i < n; i++) { ... }`, each capped at 10,000 iterations. `break;` and `continue;` act\non the innermost loop; labels and `for-in` are refused.\n\n**`let`.** `let x = <expr>;` declares a block-scoped variable; the initializer is required\n(`let x = null;`). Reassign with `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `??=`, `++` and `--`. Re-declaring in\nthe same block is refused; shadowing in a nested block is allowed. A `let` keeps its value across a\n`wait`, `wait_for_event` or `wait_for_approval`.\n\n**Refused:** regex, `new`, `typeof`, `instanceof`, `in`, `delete`, `void`, rest elements, computed keys,\nclasses, `throw`, `import`, `export`, and a function as a value (`const f = (x) => ...`).\n\n## Helpers\n\nCalled as `size(arr)`. The method form works only where the name is also a JavaScript method \u2014\n`x.trim()`, `arr.includes(v)`, `arr.at(-1)`, `arr.map(fn)`, `s.split(",")`; `arr.size()` is refused.\n\n- **Null and type**: `isNull`, `isNotNull`, `isEmpty`, `isString`, `isNumber`, `isBoolean`, `isArray`,\n `isObject`, `coalesce`, `get`, `toNumber`, `toString`, `typeOf`, `parseJson`, `toJson`\n- **Lists**: `size`, `first`, `requireFirst` (the first item, refusing an empty list \u2014 after a\n `validate` on the size it saves an `if`), `last`, `nth`, `at` (`at(arr, -1)` counts from the end),\n `includes`, `filter`, `find`, `some`, `every`, `pluck`, `sortBy`, `groupBy`, `countBy`, `unique`,\n `uniqueBy`, `compact` (drops falsy items), `flatten`, `reverse`, `slice`, `concat`, `difference`,\n `differenceBy`, `intersection`, `intersectionBy`, `list`, `reduce(arr, (acc, x) => ..., initial)`,\n `range(end)` / `range(start, end)`\n- **Math**: `sum`, `sumBy`, `mean`, `meanBy`, `minBy`, `maxBy`, `round`, `ceil`, `floor`, `min`, `max`,\n `abs`, `mod`, `pow`, `sqrt`, `clamp`, `percentage`\n- **Text**: `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`,\n `replaceAll`, `substring`, `length`, `split`, `join`, `padStart(str, length, char)`,\n `padEnd(str, length, char)`, `formatNumber(x, decimals)` (ungrouped, as `x.toFixed`),\n `formatDecimal(x, decimals, locale)` (grouped: `formatDecimal(151000, 0, "vi-VN")` is `151.000`),\n `numberToWords(x, lang?)` (an integer in words: Vietnamese, or English with `"en"`)\n- **Objects**: `keys`, `values`, `entries`, `nonNullKeys`, `pick`, `omit`, `merge`\n- **Dates**, in the workspace\'s timezone: `now`, `formatDate`, `parseDate`, `addDays`, `subDays`,\n `addHours`, `subHours`, `addMinutes`, `subMinutes`, `startOfDay`, `endOfDay`,\n `differenceInCalendarDays`, `differenceInHours`, `differenceInMinutes`, `isBefore`, `isAfter`,\n `isSameDay`, `isToday`, `isWithinRange`\n- **Other**: `formatCurrency(amount, locale, currency)`, `randomNumber(len)`,\n `randomAlphaNumeric(len)`, `sample(items)` (a random item)\n\nA date helper given a null or empty date returns null, so guard its result before comparing or\nwriting it: `differenceInCalendarDays(a, b) ?? 0`.\n\n`filter`, `find`, `some`, `every`, `sortBy`, `pluck`, `sumBy`, `meanBy`, `minBy`, `maxBy`, `groupBy`,\n`countBy`, `uniqueBy`, `differenceBy` and `intersectionBy` take a path string\n(`filter(record.items, "Status", "open")`) or a callback\n(`filter(record.items, x => x.Status == "open" && x.Amount > 100)`); `reduce` takes a callback and an\ninitial value. A callback gets `(item, idx)` (`reduce`: `(acc, item, idx)`) and reads `record`,\n`runtime`, the names in scope, loop items and an enclosing callback\'s parameters. Its body is one\nexpression \u2014 `(x) => <expr>`, `(x) => { return <expr>; }` or `function (x) { return <expr>; }`;\nseveral statements, `async` and named function expressions are refused.\n\n## Writing a field: null and undefined\n\nIn `update_records`\' `set` and `create_records`\' `records`:\n\n- `null` clears the field.\n- `undefined`, or leaving the key out, leaves the field as it is.\n\nSo passing a read that may be null (`record["fld_x"]`) keeps a value when there is one and clears the\nfield when there is none. Use `coalesce(x, fallback)` only for a real fallback value.\n\n## Examples\n\nCompare a link by id. Display text is not unique, and a link reads as a list of ids \u2014 `==` against a\nstring is a type error:\n\n```\nconst found = await query_records({\n table_id: "tbl_customers",\n filters: { node_type: "condition", field_key: "fld_name", operator: "equals", value: "ACME Corp" },\n});\nconst customer = first(found.records);\n\nif (customer && includes(record["fld_customer"], customer.id)) {\n // \u2026\n}\n```\n\nRead an uploaded sheet and add the rows a table lacks. `excel_get_rows`, a workflow step, answers every\nrow of an `.xlsx`, `.xls` or `.csv` sheet in one call, each an object keyed by the header row\'s text. A cell is a number,\n`true` or `false`, text, a date as ISO text, or null when blank, so `row["Header"]` is typed\n`string | number | boolean | null` and a helper narrows it: `toString` for text, `toNumber` for a number,\n`isString` before a date. Every cell of a `.csv` is text. `columns` keeps only the ones the body reads.\nA sheet past what one call reads is refused with its size, never cut short: the step\'s entry in\n`workflow_steps` states the bounds.\n\nA table can hold more rows than one page of `query_records` answers, so the body never reads "the\ntable" to see what it lacks. It takes the sheet fifty rows at a time \u2014 what one `create_records` call\nwrites \u2014 asks the table for those rows\' codes alone, and writes the rest in one call:\n\n```\nconst sheet = await excel_get_rows({ // a workflow step\n file_id: trigger.app_workflow.inputs.file,\n columns: ["Code", "Filed", "Amount"],\n});\n\nfor (let start = 0; start < sheet.row_count; start += 50) {\n const rows = uniqueBy(slice(sheet.rows, start, start + 50), (row) => toString(row["Code"]));\n const held = await query_records({\n table_id: "tbl_declarations",\n filters: { field_key: "fld_code", operator: "is_any_of", value: rows.map((row) => toString(row["Code"])) },\n pagination: { offset: 0, limit: 1000 },\n });\n const known = held.records.map((r) => toString(r.data["fld_code"]));\n const fresh = filter(rows, (row) => !includes(known, toString(row["Code"])));\n if (size(fresh) > 0) {\n await create_records({\n table_id: "tbl_declarations",\n records: fresh.map((row) => ({\n fld_code: toString(row["Code"]),\n fld_filed: isString(row["Filed"]) ? row["Filed"] : null,\n fld_amount: toNumber(row["Amount"]),\n })),\n });\n }\n}\n```\n\n## Table lifecycle workflows\n\nA lifecycle workflow is bound to one table and one event, and its source has no `on({...})` line:\n\n- `after_create` \u2014 a create, and a draft\'s submit (the moment it becomes a record).\n- `after_update` \u2014 every field edit, a draft\'s included.\n- `after_delete`.\n\nA lifecycle workflow runs after the write commits, with every step; its errors are logged and never\nfail the write. What refuses a write before it lands is a table check (see **Table\nchecks**). An `after_update` workflow that writes its own table saves with a `loop_potential`\nwarning: its write fires it again.\n\n| reads | on |\n|---|---|\n| `record` | every event \u2014 the new record on create, the record as it now stands on update, the deleted record on delete |\n| `prev_record` | updates and deletes \u2014 the record before |\n| `changes["fld_x"]?.next_value` / `?.prev_value` | updates \u2014 the fields that changed |\n\n### Gating with `if_source`\n\n`if_source` is one JavaScript expression, reading what the body reads, tested before the workflow\nruns. When it is falsy the workflow does not run at all \u2014 no execution, no log. Use it so a workflow\nthat cares about some events only skips the rest:\n\n- a status reaching an option: `changes["fld_status"]?.next_value == "opt_done"`\n- a field cleared: `isNull(record["fld_assignee"])`\n- a direct write, not a workflow\'s cascade:\n `includes(["member", "chat_agent", "api_client"], runtime.change_origin.type)` \u2014 name every\n direct-write origin rather than testing for one: `member` is a person in the browser, and the same\n edit over MCP, the CLI or the API arrives as `api_client`.\n\nIn `if_source`, `runtime.change_origin` is the origin of the write that fired it; in the body, it is\nthe workflow\'s own.\n\n## Table checks\n\nA table check is one condition its table\'s record writes must not meet, declared beside the table\'s\n`unique`. It is not a workflow: it reads the write and a few lookups and answers yes or no, so it\ncannot change data. It runs inside the write, on every create, update or delete it is `on`, whichever\nsurface makes it \u2014 a person, the API, the CLI, an agent, a workflow. A refusal writes nothing and\nanswers with the check\'s message, under its field when the check names one, and names the check.\n\nOn the CLI or in chat, `set_table_check` creates one, or changes one when given its `table_check_id`\nand only the parts that change, and `remove_table_check` removes one. `get_table` lists a table\'s\nchecks in the source they are written in.\n\n| part | what it holds |\n|---|---|\n| `on` | the writes it checks: any of `create`, `update`, `delete` |\n| `when` | optional expression; the check runs only on a write where it is truthy. It cannot read lookups. |\n| `lookups` | optional, by name, the rows to read: `{ table_id, filter, limit }`, the filter as `query_records` takes it, `limit` from 1 to 50 (50 when left out). A filter value may read the record. |\n| `fail_when` | expression; truthy refuses the write |\n| `message` | expression for the refusal\'s text \u2014 a fixed text is a quoted string |\n| `field_key` | optional; the field the refusal is shown under |\n\nEvery expression is written in the grammar above and reads:\n\n- `record` \u2014 the record as it will be stored; on a delete, the record being deleted.\n- `prev_record` \u2014 on an update or a delete, the record before.\n- `changes` \u2014 on an update, the fields it changes.\n- `lookups.<name>` \u2014 in `fail_when` and `message`, the rows that lookup read.\n- `runtime.timezone`, `runtime.workspace_id`, `runtime.organization_id`, `runtime.change_origin` (the\n write\'s origin), `runtime.triggered_by_member_id` (the member writing; null for a write no member\n made), and `now()`.\n\nA check on several operations reads only what all of them carry, so one on creates and updates reads\nneither `prev_record` nor `changes`.\n\nA lookup reads with the access of the member who last saved the check, so saving one requires reading\nevery table its lookups read. It reads committed rows: two writes at the same moment can each pass a\ncheck the other would fail, so values no two rows may share are the table\'s `unique`, not a check.\n\nA `when`, a lookup or a `fail_when` that cannot decide \u2014 it fails, or the write\'s time runs out \u2014\nrefuses the write. A draft is checked when it is submitted, as a create; its edits before that are\nnot. A restored record is checked as a create. Cloning a table copies its checks to the member who\nclones it, and is refused when that member cannot read a table a check\'s lookup reads.\n\nOne open order per reference, a row never counting against itself:\n\n```\n{\n "table_id": "tbl_orders",\n "name": "One open order per reference",\n "on": ["create", "update"],\n "when": "isNull(record[\\"fld_closed_at\\"])",\n "lookups": {\n "same_ref": "{ table_id: \\"tbl_orders\\", filter: { node_type: \\"group\\", logic: \\"and\\", children: [ { field_key: \\"fld_ref\\", operator: \\"equals\\", value: record[\\"fld_ref\\"] }, { field_key: \\"fld_closed_at\\", operator: \\"is_empty\\" } ] }, limit: 2 }"\n },\n "fail_when": "some(lookups.same_ref, (r) => r.id != record.id)",\n "message": "\\"An open order already has this reference.\\"",\n "field_key": "fld_ref"\n}\n```\n';
|
|
44398
44559
|
|
|
44399
44560
|
// docs/workflow_steps.md
|
|
44400
|
-
var workflow_steps_default = '<!-- Generated by backend/scripts/generate_workflow_steps_doc.ts from each step\'s schemas \u2014 never edit by hand. -->\n\n# Workflow steps\n\nThe steps only a workflow body runs: a connection\'s (an invoice issuer, a mailbox, \u2026) and the platform\'s own. Each is\ncalled as `await <step>({ ...inputs })` (see `workflows`). `lotics tools <name>` prints any tool\'s inputs and answer,\nthese steps among them.\n\n**How a step fails.** A step that is refused \u2014 an input the schema rejects, a connection the workspace lacks, an\nanswer the provider refuses \u2014 ends the run, and the run\'s error reads `[<step>] <reason>`. Inside `try { \u2026 } catch\n(e) { \u2026 }` the body handles it instead: `e` is `{ message, type, step_id?, detail? }`, `message` the reason as\nabove. A provider\'s refusal carries the provider\'s own words.\n\n## bkav\n\n### bkav_attach_files (workflow step)\n\n- Attach workspace files (a bi\xEAn b\u1EA3n, a b\u1EA3ng k\xEA) to an invoice on Bkav.\n- Bkav accepts PDF and Excel, at most 1 MB each.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | | |\n| `partner_invoice_id` | string | | Your PartnerInvoiceStringID, when invoice_guid is not set |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_cancel_invoices (workflow step)\n\n- Cancel issued invoices.\n- A cancelled invoice still reports to the tax authority; correcting one instead is a replacement or adjustment through bkav_submit_invoices. (workflow step)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | | InvoiceGUIDs Bkav returned |\n| `partner_invoice_ids` | string[] | | Your PartnerInvoiceStringIDs |\n| `reason` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_create_bill (workflow step)\n\n- Record a cash-register bill on Bkav.\n- Field names are Bkav\'s.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `bill` | object | yes | |\n| `bill.Bill` | object | yes | |\n| `bill.Bill.BillCode` | number | yes | |\n| `bill.Bill.OriginalBillCode` | number | | |\n| `bill.Bill.BillDate` | string | yes | |\n| `bill.Bill.BillStatusID` | number | yes | |\n| `bill.Bill.PayMethod` | string | | |\n| `bill.Bill.Note` | string | | |\n| `bill.ListBillDetails` | object[] | yes | |\n| `bill.ListBillDetails[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `bill.ListBillDetails[].IsDiscount` | boolean | | |\n| `bill.ListBillDetails[].ItemCode` | string | | |\n| `bill.ListBillDetails[].ItemName` | string | yes | |\n| `bill.ListBillDetails[].UnitName` | string | | |\n| `bill.ListBillDetails[].Qty` | number | | |\n| `bill.ListBillDetails[].Price` | number | | |\n| `bill.ListBillDetails[].Amount` | number | | |\n| `bill.ListBillDetails[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `bill.ListBillDetails[].TaxRate` | number | | |\n| `bill.ListBillDetails[].TaxAmount` | number | | |\n| `bill.ListBillDetails[].OtherAmount` | number | | |\n| `bill.ListBillDetails[].DiscountRate` | number | | |\n| `bill.ListBillDetails[].DiscountAmount` | number | | |\n| `bill.ListBillDetails[].UserDefineDetails` | string | | |\n| `bill.ListBillDetails[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `bill.ListBillDetails[].SpecialtyItems` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `created` | true | yes | |\n\n### bkav_create_invoice_from_bills (workflow step)\n\n- Raise one invoice from cash-register bills, by their BillGUIDs.\n- Field names are Bkav\'s.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `input` | object | yes | |\n| `input.Invoice` | object | yes | |\n| `input.Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `input.Invoice.InvoiceDate` | string | yes | ISO date |\n| `input.Invoice.BuyerCode` | string | | |\n| `input.Invoice.BuyerName` | string | | |\n| `input.Invoice.BuyerTaxCode` | string | | |\n| `input.Invoice.BuyerUnitName` | string | | |\n| `input.Invoice.BuyerAddress` | string | | |\n| `input.Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `input.Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `input.Invoice.ReceiverEmail` | string | | |\n| `input.Invoice.ReceiverMobile` | string | | |\n| `input.Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.Note` | string | | |\n| `input.Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.CurrencyID` | string | | |\n| `input.Invoice.ExchangeRate` | number | | |\n| `input.Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `input.Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `input.Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `input.Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `input.Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `input.Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `input.Invoice.isBTH` | boolean | | |\n| `input.Invoice.CCCD` | string | | |\n| `input.Invoice.PassportNumber` | string | | |\n| `input.Invoice.FiscalCodes` | string | | |\n| `input.Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `input.Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `input.Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `input.BillGUIDs` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_delete_invoices (workflow step)\n\n- Delete invoices that were never issued.\n- A numbered one can be deleted only if its number is the highest in its range.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | | InvoiceGUIDs Bkav returned |\n| `partner_invoice_ids` | string[] | | Your PartnerInvoiceStringIDs |\n| `reason` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_explain_invoice_error (workflow step)\n\n- Explain an erroneous invoice to the tax authority without replacing or adjusting it, optionally answering one of its notices.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `reason` | string | yes | |\n| `notice_number` | string | | S\u1ED1 of the authority\'s notice being answered |\n| `notice_date` | string | | yyyy-mm-dd; with notice_number |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise Msg says why |\n| `results[].Msg` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_explain_replaced_invoice (workflow step)\n\n- Explain to the tax authority an invoice that was replaced or adjusted.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `reason` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise Msg says why |\n| `results[].Msg` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_get_bill (workflow step)\n\n- Read a cash-register bill.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `seller_tax_code` | string | yes | |\n| `organization_code` | string | yes | |\n| `bill_code` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `bill` | object | yes | |\n\n### bkav_get_error_notice_pdf (workflow step)\n\n- Save the PDF of an invoice\'s 04/SS error notice as a workspace file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_file_by_lookup_code (workflow step)\n\n- Save an invoice\'s display copy, conversion copy or XML as a workspace file, found by the m\xE3 tra c\u1EE9u the buyer was sent.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `lookup_code` | string | yes | M\xE3 tra c\u1EE9u |\n| `kind` | "display" \\\\| "converted" \\\\| "xml" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_invoice (workflow step)\n\n- Read one invoice as Bkav holds it \u2014 header, lines, status and m\xE3 tra c\u1EE9u.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `id` | string | yes | Your PartnerInvoiceStringID or Bkav\'s InvoiceGUID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### bkav_get_invoice_file (workflow step)\n\n- Save an invoice\'s PDF or signed XML as a workspace file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n| `format` | "pdf" \\\\| "xml" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_invoice_history (workflow step)\n\n- Read everything that happened to an invoice on Bkav, oldest first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `entries` | object[] | yes | |\n\n### bkav_get_invoice_link (workflow step)\n\n- Get a download link for an invoice\'s display copy, conversion copy, or its 04/SS error notice.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n| `kind` | "display" \\\\| "converted" \\\\| "notice_04ss" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `url` | string | yes | |\n\n### bkav_get_invoice_status (workflow step)\n\n- Read an invoice\'s status on Bkav.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice_status_id` | number | yes | 1 m\u1EDBi t\u1EA1o, 2 \u0111\xE3 ph\xE1t h\xE0nh, 3 \u0111\xE3 hu\u1EF7, 5\u20138 ch\u1EDD/\u0111\xE3 thay th\u1EBF\u2013\u0111i\u1EC1u ch\u1EC9nh, 11 \u0111\xE3 c\u1EA5p s\u1ED1 ch\u1EDD k\xFD |\n\n### bkav_get_listing (workflow step)\n\n- Save the b\u1EA3ng k\xEA of one invoice type over a window as a workspace spreadsheet.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_type_id` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, \u2026 |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_report_bc26 (workflow step)\n\n- Save the BC26/AC invoice usage report for a window as a workspace XML file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_tax_status (workflow step)\n\n- Read the tax authority\'s status for an invoice and its m\xE3 c\u1EE7a CQT once granted.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `statuses` | object[] | yes | |\n| `statuses[].BkavStatus` | number | yes | InvoiceStatusID: 1 m\u1EDBi t\u1EA1o, 2 \u0111\xE3 ph\xE1t h\xE0nh, 3 \u0111\xE3 hu\u1EF7, \u2026 |\n| `statuses[].TaxStatus` | number | yes | 32 waiting, 33 accepted, 34\u201339 returned or failed |\n| `statuses[].TaxAuthorityCode` | string \\\\| null | yes | M\xE3 c\u1EE7a C\u01A1 quan thu\u1EBF, once granted |\n| `statuses[].ErrorContent` | string \\\\| null | yes | |\n\n### bkav_list_invoices_by_date (workflow step)\n\n- List invoices dated within a window, one page at a time.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n| `page` | number | | 1-based |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoices` | object[] | yes | |\n\n### bkav_list_invoices_by_number (workflow step)\n\n- List invoices of one m\u1EABu s\u1ED1 and k\xFD hi\u1EC7u by number range.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `form` | string | yes | M\u1EABu s\u1ED1 |\n| `serial` | string | yes | K\xFD hi\u1EC7u |\n| `from_number` | number | yes | |\n| `to_number` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoices` | object[] | yes | |\n\n### bkav_lookup_company (workflow step)\n\n- Look up a company in the tax registry by M\xE3 s\u1ED1 thu\u1EBF, through the Bkav account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `tax_code` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `company` | object | yes | |\n| `company.MaSoThue` | string | yes | |\n| `company.TenChinhThuc` | string | yes | |\n| `company.DiaChiGiaoDichChinh` | string | yes | |\n| `company.DiaChiGiaoDichPhu` | string | yes | |\n| `company.TrangThaiHoatDong` | string | yes | |\n| `company.ChuDoanhNghiep` | string | | |\n| `company.SoDienThoai` | string \\\\| null | | |\n| `company.LastUpdate` | string | | |\n\n### bkav_send_lookup_email (workflow step)\n\n- Send an invoice\'s lookup email to the buyer again, or to another email or phone.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `email` | string | | |\n| `mobile` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `sent` | true | yes | |\n\n### bkav_sign_invoices (workflow step)\n\n- Sign \u2014 issue \u2014 invoices with the account\'s HSM signature, making them tax-registered documents.\n- An account that signs with a USB token cannot sign this way.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `signed` | string[] | yes | |\n\n### bkav_submit_invoices (workflow step)\n\n- Create, replace or adjust invoices on Bkav, in Bkav\'s field names.\n- Each carries PartnerInvoiceStringID, your own id for it, which Bkav uses to refuse a duplicate.\n- Attachments are not accepted here.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `kind` | "create_draft" \\\\| "create" \\\\| "create_draft_own_serial" \\\\| "create_own_number" \\\\| "create_own_serial" \\\\| "replace_draft" \\\\| "adjust_draft" \\\\| "adjust_discount_draft" \\\\| "replace" \\\\| "adjust" \\\\| "adjust_external_draft" \\\\| "adjust_discount" \\\\| "adjust_external" \\\\| "replace_external" \\\\| "replace_external_draft" | yes | create_draft: no number, deletable. create: Bkav numbers it. *_own_*: your form/serial. replace_* / adjust_*: each invoice names its original in Invoice.OriginalInvoiceIdentify and gives Invoice.Reason. *_external*: corrects an invoice issued outside Bkav; its first line is an ItemTypeID 4 description of it. *_draft kinds stay unnumbered. |\n| `invoices` | object[] | yes | |\n| `invoices[].Invoice` | object | yes | |\n| `invoices[].Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `invoices[].Invoice.InvoiceDate` | string | yes | ISO date |\n| `invoices[].Invoice.BuyerCode` | string | | |\n| `invoices[].Invoice.BuyerName` | string | | |\n| `invoices[].Invoice.BuyerTaxCode` | string | | |\n| `invoices[].Invoice.BuyerUnitName` | string | | |\n| `invoices[].Invoice.BuyerAddress` | string | | |\n| `invoices[].Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `invoices[].Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `invoices[].Invoice.ReceiverEmail` | string | | |\n| `invoices[].Invoice.ReceiverMobile` | string | | |\n| `invoices[].Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.Note` | string | | |\n| `invoices[].Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.CurrencyID` | string | | |\n| `invoices[].Invoice.ExchangeRate` | number | | |\n| `invoices[].Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `invoices[].Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `invoices[].Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `invoices[].Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `invoices[].Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `invoices[].Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `invoices[].Invoice.isBTH` | boolean | | |\n| `invoices[].Invoice.CCCD` | string | | |\n| `invoices[].Invoice.PassportNumber` | string | | |\n| `invoices[].Invoice.FiscalCodes` | string | | |\n| `invoices[].Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `invoices[].Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `invoices[].Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `invoices[].ListInvoiceDetailsWS` | object[] | yes | |\n| `invoices[].ListInvoiceDetailsWS[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].IsDiscount` | boolean | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemCode` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemName` | string | yes | |\n| `invoices[].ListInvoiceDetailsWS[].UnitName` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].Qty` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Price` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Amount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].TaxRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].OtherAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].UserDefineDetails` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `invoices[].ListInvoiceDetailsWS[].SpecialtyItems` | string | | |\n| `invoices[].PartnerInvoiceStringID` | string | yes | |\n| `invoices[].AttachListNumber` | string | | S\u1ED1 of a b\u1EA3ng k\xEA the invoice refers to |\n| `invoices[].AttachListDate` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_update_invoices (workflow step)\n\n- Change invoices not yet issued. by names how each invoice is found: partner (its partner id), number (Invoice.InvoiceForm + InvoiceSerial + InvoiceNo) or guid (Invoice.InvoiceGUID).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `by` | "partner" \\\\| "number" \\\\| "guid" | yes | |\n| `invoices` | object[] | yes | |\n| `invoices[].Invoice` | object | yes | |\n| `invoices[].Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `invoices[].Invoice.InvoiceDate` | string | yes | ISO date |\n| `invoices[].Invoice.BuyerCode` | string | | |\n| `invoices[].Invoice.BuyerName` | string | | |\n| `invoices[].Invoice.BuyerTaxCode` | string | | |\n| `invoices[].Invoice.BuyerUnitName` | string | | |\n| `invoices[].Invoice.BuyerAddress` | string | | |\n| `invoices[].Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `invoices[].Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `invoices[].Invoice.ReceiverEmail` | string | | |\n| `invoices[].Invoice.ReceiverMobile` | string | | |\n| `invoices[].Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.Note` | string | | |\n| `invoices[].Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.CurrencyID` | string | | |\n| `invoices[].Invoice.ExchangeRate` | number | | |\n| `invoices[].Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `invoices[].Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `invoices[].Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `invoices[].Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `invoices[].Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `invoices[].Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `invoices[].Invoice.isBTH` | boolean | | |\n| `invoices[].Invoice.CCCD` | string | | |\n| `invoices[].Invoice.PassportNumber` | string | | |\n| `invoices[].Invoice.FiscalCodes` | string | | |\n| `invoices[].Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `invoices[].Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `invoices[].Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `invoices[].ListInvoiceDetailsWS` | object[] | yes | |\n| `invoices[].ListInvoiceDetailsWS[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].IsDiscount` | boolean | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemCode` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemName` | string | yes | |\n| `invoices[].ListInvoiceDetailsWS[].UnitName` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].Qty` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Price` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Amount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].TaxRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].OtherAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].UserDefineDetails` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `invoices[].ListInvoiceDetailsWS[].SpecialtyItems` | string | | |\n| `invoices[].PartnerInvoiceStringID` | string | yes | |\n| `invoices[].AttachListNumber` | string | | S\u1ED1 of a b\u1EA3ng k\xEA the invoice refers to |\n| `invoices[].AttachListDate` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n## drive\n\n### drive_export_file (workflow step)\n\n- Upload a Lotics file to the member\'s Google Drive.\n- The file is uploaded as-is \u2014 a .docx stays a .docx, a .xlsx stays a .xlsx.\n- It is deliberately NOT converted into a Google Doc or Sheet: conversion re-flows the layout of a generated document, and a native Google file is live, so its figures would drift away from the record they came from.\n- Use this to archive a finished document, the way you would attach it to an email.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `file_id` | string | yes | Lotics file ID to upload |\n| `folder_id` | string | | Drive folder ID to place the file in; omit for the member\'s Drive root |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `drive_file_id` | string | yes | Drive file ID of the uploaded copy |\n| `web_view_link` | string \\\\| null | yes | URL to open the uploaded file in Drive |\n\n### drive_import_file (workflow step)\n\n- Import a Google Drive file into the workspace as a Lotics file, and return its file id.\n- Google Docs arrive as .docx, Sheets as .xlsx, Slides as .pptx; everything else transfers unchanged. **This is also how you READ a Drive file.** There is no separate Drive read tool: import the file, then use `view_files` on the returned id. (workflow step, chat)\n- That path handles scanned documents \u2014 a photographed B/L or a stamped invoice inside a .docx reaches the model as a picture.\n- Exporting Drive content as plain text would return an empty document for exactly those files.\n- Folders, Forms, shortcuts and other Drive items with no file form cannot be imported.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `file_id` | string | yes | Drive file ID, from drive_query_files (workflow step, chat) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | Lotics file ID \u2014 pass this to view_files to read the contents (workflow step, chat) |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n### drive_query_files (workflow step)\n\n- Search the member\'s Google Drive using Drive query syntax. ## Query syntax Terms are `field operator value`; combine with `and` / `or` / `not`.\n- String values are single-quoted. - Name: `name = \'Rate card\'`, `name contains \'invoice\'` - Full text (body + title): `fullText contains \'CMA CGM\'` - Type: `mimeType = \'application/vnd.google-apps.spreadsheet\'`, `mimeType contains \'pdf\'` - Folder: `\'<folder_id>\' in parents` - Time (RFC 3339): `modifiedTime > \'2026-01-01T00:00:00\'` - Exclude deleted: `trashed = false` (always worth adding) - Starred / shared: `starred = true`, `sharedWithMe = true` Google-native types: `application/vnd.google-apps.document` (Docs), `.spreadsheet` (Sheets), `.presentation` (Slides), `.folder`.\n- Examples: - `name contains \'packing list\' and trashed = false` - `\'1a2b3c\' in parents and modifiedTime > \'2026-08-01T00:00:00\'` - `fullText contains \'demurrage\' and mimeType = \'application/vnd.google-apps.document\'` Results carry enough metadata (name, type, modified time, size, owner) to pick a file WITHOUT importing it.\n- Import only the one you actually need \u2014 `drive_import_file` persists a copy in the workspace. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `query` | string | yes | Drive query syntax expression |\n| `max_result` | number | yes | Maximum number of files to retrieve (1-50) |\n| `order_by` | string | | Sort order, e.g. \'modifiedTime desc\', \'name\' |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `files` | object[] | yes | List of matching Drive files |\n| `files[].id` | string | yes | Drive file ID \u2014 pass this to drive_import_file (workflow step, chat) |\n| `files[].name` | string | yes | File name as it appears in Drive |\n| `files[].mime_type` | string | yes | Drive MIME type; \'application/vnd.google-apps.*\' means Google-native |\n| `files[].modified_time` | string \\\\| null | yes | RFC 3339 timestamp of the last modification |\n| `files[].size_bytes` | number \\\\| null | yes | Size in bytes; null for Google-native files, which have no stored size until exported |\n| `files[].owner` | string \\\\| null | yes | Email address of the file\'s owner |\n| `files[].web_view_link` | string \\\\| null | yes | URL to open the file in Drive |\n\n## evaluate\n\n### evaluate_formula (workflow step)\n\n- Compute a formula over one record as its table\'s own formula fields compute: fields referenced as `{fld_\u2026}`, lookups and rollups read as the row holds them.\n- Writes nothing.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `record_id` | string | yes | |\n| `expression` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `value` | number \\\\| string \\\\| boolean \\\\| null | yes | |\n\n## excel\n\n### excel_create_file (workflow step)\n\n- Create an Excel file with styled column headers and auto-filter enabled.\n- Input: { filename, sheets: [{ name, columns: [{ header, width? }], rows: unknown[][] }] } Headers are bold, centered, bordered, with auto-filter on the header row.\n- Best for producing polished report-style spreadsheets.\n- Returns a new file_id \u2014 use it for subsequent operations.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `sheets` | object[] | yes | |\n| `sheets[].name` | string | yes | |\n| `sheets[].columns` | object[] | yes | |\n| `sheets[].columns[].header` | string | yes | |\n| `sheets[].columns[].width` | number | | |\n| `sheets[].rows` | any[][] | yes | 2D array of cell values |\n| `filename` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### excel_find_cells (workflow step)\n\n- Search for cells matching a text query in an Excel worksheet.\n- Input: { file_id, worksheet: string\\|number, query: "search text", match_type?: "contains"\\|"exact"\\|"regex", max_results?: number } Default match_type is "contains" (case-insensitive).\n- Returns matching cell addresses, values, and positions.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `query` | string | yes | Text to find. |\n| `match_type` | "exact" \\\\| "contains" \\\\| "regex" | | How to match: exact, contains (default), or regex |\n| `max_results` | number | | Max matches to return |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `matches` | object[] | yes | |\n| `matches[].cell` | string | yes | Cell address, e.g. B3 |\n| `matches[].value` | string | yes | Cell value as string |\n| `matches[].row` | number | yes | Row number |\n| `matches[].column` | string | yes | Column letter |\n| `total_matches` | number | yes | Total number of matches found |\n\n### excel_format_range (workflow step)\n\n- Apply formatting to a cell range (fonts, fills, borders, alignment, number formats). range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:D10".\n- Never use "B2" alone.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `format` | object | yes | |\n| `format.font_bold` | boolean | | |\n| `format.font_italic` | boolean | | |\n| `format.font_size` | number | | |\n| `format.font_color` | string | | Hex color like #FF0000 |\n| `format.font_name` | string | | |\n| `format.font_underline` | "single" \\\\| "double" | | |\n| `format.font_strike` | boolean | | |\n| `format.background_color` | string | | Hex color like #FFFF00 |\n| `format.horizontal_align` | "left" \\\\| "center" \\\\| "right" | | |\n| `format.vertical_align` | "top" \\\\| "middle" \\\\| "bottom" | | |\n| `format.wrap_text` | boolean | | |\n| `format.number_format` | string | | Excel number format code (e.g. #,##0.00, dd/mm/yyyy) |\n| `format.border_top` | object | | |\n| `format.border_top.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_top.width` | number | | |\n| `format.border_top.color` | string | | |\n| `format.border_bottom` | object | | |\n| `format.border_bottom.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_bottom.width` | number | | |\n| `format.border_bottom.color` | string | | |\n| `format.border_left` | object | | |\n| `format.border_left.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_left.width` | number | | |\n| `format.border_left.color` | string | | |\n| `format.border_right` | object | | |\n| `format.border_right.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_right.width` | number | | |\n| `format.border_right.color` | string | | |\n| `column_widths` | object | | Column letter \u2192 width mapping |\n| `row_heights` | object | | Row number \u2192 height mapping |\n| `auto_filter` | string | | Range for an auto-filter (e.g. A1:D1); one over a table turns on that table\'s own filter |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `formatted_cells` | number | yes | |\n\n### excel_get_range (workflow step)\n\n- Read cell values from a range in an Excel worksheet. range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:D10".\n- Never use "B2" alone. worksheet: use sheet name (preferred) or 0-based index.\n- Use view_file first to see available worksheets.\n- Returns data keyed by row number then column letter, e.g. { "1": { "A": "Hello" } }.\n- Maximum 1000 cells per call \u2014 use smaller ranges and paginate if needed.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `options` | object | | Options for data retrieval |\n| `options.include_formulas` | boolean | | Include cell formulas instead of calculated values |\n| `options.include_formatting` | boolean | | Include cell formatting information |\n| `options.max_cell_length` | number | | Max characters returned per cell; longer text is truncated with an ellipsis. Pass -1 to return cells in full \u2014 use when reading free-text columns like notes. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | The worksheet name |\n| `range` | string | yes | The requested range |\n| `data` | object | yes | Cell values keyed by row number then column letter |\n| `formatting` | object | | Cell formatting keyed by row then column |\n| `truncated_cells` | object[] | | Cells with truncated content |\n| `truncated_cells[].cell` | string | yes | Cell address |\n| `truncated_cells[].original_length` | number | yes | Original text length |\n\n### excel_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';
|
|
44561
|
+
var workflow_steps_default = '<!-- Generated by backend/scripts/generate_workflow_steps_doc.ts from each step\'s schemas \u2014 never edit by hand. -->\n\n# Workflow steps\n\nThe steps only a workflow body runs: a connection\'s (an invoice issuer, a mailbox, \u2026) and the platform\'s own. Each is\ncalled as `await <step>({ ...inputs })` (see `workflows`). `lotics tools <name>` prints any tool\'s inputs and answer,\nthese steps among them.\n\n**How a step fails.** A step that is refused \u2014 an input the schema rejects, a connection the workspace lacks, an\nanswer the provider refuses \u2014 ends the run, and the run\'s error reads `[<step>] <reason>`. Inside `try { \u2026 } catch\n(e) { \u2026 }` the body handles it instead: `e` is `{ message, type, step_id?, detail? }`, `message` the reason as\nabove. A provider\'s refusal carries the provider\'s own words.\n\n## bkav\n\n### bkav_attach_files (workflow step)\n\n- Attach workspace files (a bi\xEAn b\u1EA3n, a b\u1EA3ng k\xEA) to an invoice on Bkav.\n- Bkav accepts PDF and Excel, at most 1 MB each.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | | |\n| `partner_invoice_id` | string | | Your PartnerInvoiceStringID, when invoice_guid is not set |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_cancel_invoices (workflow step)\n\n- Cancel issued invoices.\n- A cancelled invoice still reports to the tax authority; correcting one instead is a replacement or adjustment through bkav_submit_invoices. (workflow step)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | | InvoiceGUIDs Bkav returned |\n| `partner_invoice_ids` | string[] | | Your PartnerInvoiceStringIDs |\n| `reason` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_create_bill (workflow step)\n\n- Record a cash-register bill on Bkav.\n- Field names are Bkav\'s.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `bill` | object | yes | |\n| `bill.Bill` | object | yes | |\n| `bill.Bill.BillCode` | number | yes | |\n| `bill.Bill.OriginalBillCode` | number | | |\n| `bill.Bill.BillDate` | string | yes | |\n| `bill.Bill.BillStatusID` | number | yes | |\n| `bill.Bill.PayMethod` | string | | |\n| `bill.Bill.Note` | string | | |\n| `bill.ListBillDetails` | object[] | yes | |\n| `bill.ListBillDetails[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `bill.ListBillDetails[].IsDiscount` | boolean | | |\n| `bill.ListBillDetails[].ItemCode` | string | | |\n| `bill.ListBillDetails[].ItemName` | string | yes | |\n| `bill.ListBillDetails[].UnitName` | string | | |\n| `bill.ListBillDetails[].Qty` | number | | |\n| `bill.ListBillDetails[].Price` | number | | |\n| `bill.ListBillDetails[].Amount` | number | | |\n| `bill.ListBillDetails[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `bill.ListBillDetails[].TaxRate` | number | | |\n| `bill.ListBillDetails[].TaxAmount` | number | | |\n| `bill.ListBillDetails[].OtherAmount` | number | | |\n| `bill.ListBillDetails[].DiscountRate` | number | | |\n| `bill.ListBillDetails[].DiscountAmount` | number | | |\n| `bill.ListBillDetails[].UserDefineDetails` | string | | |\n| `bill.ListBillDetails[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `bill.ListBillDetails[].SpecialtyItems` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `created` | true | yes | |\n\n### bkav_create_invoice_from_bills (workflow step)\n\n- Raise one invoice from cash-register bills, by their BillGUIDs.\n- Field names are Bkav\'s.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `input` | object | yes | |\n| `input.Invoice` | object | yes | |\n| `input.Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `input.Invoice.InvoiceDate` | string | yes | ISO date |\n| `input.Invoice.BuyerCode` | string | | |\n| `input.Invoice.BuyerName` | string | | |\n| `input.Invoice.BuyerTaxCode` | string | | |\n| `input.Invoice.BuyerUnitName` | string | | |\n| `input.Invoice.BuyerAddress` | string | | |\n| `input.Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `input.Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `input.Invoice.ReceiverEmail` | string | | |\n| `input.Invoice.ReceiverMobile` | string | | |\n| `input.Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.Note` | string | | |\n| `input.Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `input.Invoice.CurrencyID` | string | | |\n| `input.Invoice.ExchangeRate` | number | | |\n| `input.Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `input.Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `input.Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `input.Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `input.Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `input.Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `input.Invoice.isBTH` | boolean | | |\n| `input.Invoice.CCCD` | string | | |\n| `input.Invoice.PassportNumber` | string | | |\n| `input.Invoice.FiscalCodes` | string | | |\n| `input.Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `input.Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `input.Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `input.BillGUIDs` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_delete_invoices (workflow step)\n\n- Delete invoices that were never issued.\n- A numbered one can be deleted only if its number is the highest in its range.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | | InvoiceGUIDs Bkav returned |\n| `partner_invoice_ids` | string[] | | Your PartnerInvoiceStringIDs |\n| `reason` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_explain_invoice_error (workflow step)\n\n- Explain an erroneous invoice to the tax authority without replacing or adjusting it, optionally answering one of its notices.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `reason` | string | yes | |\n| `notice_number` | string | | S\u1ED1 of the authority\'s notice being answered |\n| `notice_date` | string | | yyyy-mm-dd; with notice_number |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise Msg says why |\n| `results[].Msg` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_explain_replaced_invoice (workflow step)\n\n- Explain to the tax authority an invoice that was replaced or adjusted.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `reason` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise Msg says why |\n| `results[].Msg` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_get_bill (workflow step)\n\n- Read a cash-register bill.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `seller_tax_code` | string | yes | |\n| `organization_code` | string | yes | |\n| `bill_code` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `bill` | object | yes | |\n\n### bkav_get_error_notice_pdf (workflow step)\n\n- Save the PDF of an invoice\'s 04/SS error notice as a workspace file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_file_by_lookup_code (workflow step)\n\n- Save an invoice\'s display copy, conversion copy or XML as a workspace file, found by the m\xE3 tra c\u1EE9u the buyer was sent.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `lookup_code` | string | yes | M\xE3 tra c\u1EE9u |\n| `kind` | "display" \\\\| "converted" \\\\| "xml" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_invoice (workflow step)\n\n- Read one invoice as Bkav holds it \u2014 header, lines, status and m\xE3 tra c\u1EE9u.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `id` | string | yes | Your PartnerInvoiceStringID or Bkav\'s InvoiceGUID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### bkav_get_invoice_file (workflow step)\n\n- Save an invoice\'s PDF or signed XML as a workspace file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n| `format` | "pdf" \\\\| "xml" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_invoice_history (workflow step)\n\n- Read everything that happened to an invoice on Bkav, oldest first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `entries` | object[] | yes | |\n\n### bkav_get_invoice_link (workflow step)\n\n- Get a download link for an invoice\'s display copy, conversion copy, or its 04/SS error notice.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `partner_invoice_id` | string | yes | Your PartnerInvoiceStringID |\n| `kind` | "display" \\\\| "converted" \\\\| "notice_04ss" | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `url` | string | yes | |\n\n### bkav_get_invoice_status (workflow step)\n\n- Read an invoice\'s status on Bkav.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice_status_id` | number | yes | 1 m\u1EDBi t\u1EA1o, 2 \u0111\xE3 ph\xE1t h\xE0nh, 3 \u0111\xE3 hu\u1EF7, 5\u20138 ch\u1EDD/\u0111\xE3 thay th\u1EBF\u2013\u0111i\u1EC1u ch\u1EC9nh, 11 \u0111\xE3 c\u1EA5p s\u1ED1 ch\u1EDD k\xFD |\n\n### bkav_get_listing (workflow step)\n\n- Save the b\u1EA3ng k\xEA of one invoice type over a window as a workspace spreadsheet.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_type_id` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, \u2026 |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_report_bc26 (workflow step)\n\n- Save the BC26/AC invoice usage report for a window as a workspace XML file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### bkav_get_tax_status (workflow step)\n\n- Read the tax authority\'s status for an invoice and its m\xE3 c\u1EE7a CQT once granted.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `statuses` | object[] | yes | |\n| `statuses[].BkavStatus` | number | yes | InvoiceStatusID: 1 m\u1EDBi t\u1EA1o, 2 \u0111\xE3 ph\xE1t h\xE0nh, 3 \u0111\xE3 hu\u1EF7, \u2026 |\n| `statuses[].TaxStatus` | number | yes | 32 waiting, 33 accepted, 34\u201339 returned or failed |\n| `statuses[].TaxAuthorityCode` | string \\\\| null | yes | M\xE3 c\u1EE7a C\u01A1 quan thu\u1EBF, once granted |\n| `statuses[].ErrorContent` | string \\\\| null | yes | |\n\n### bkav_list_invoices_by_date (workflow step)\n\n- List invoices dated within a window, one page at a time.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `from_date` | string | yes | yyyy-mm-dd |\n| `to_date` | string | yes | yyyy-mm-dd |\n| `page` | number | | 1-based |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoices` | object[] | yes | |\n\n### bkav_list_invoices_by_number (workflow step)\n\n- List invoices of one m\u1EABu s\u1ED1 and k\xFD hi\u1EC7u by number range.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `form` | string | yes | M\u1EABu s\u1ED1 |\n| `serial` | string | yes | K\xFD hi\u1EC7u |\n| `from_number` | number | yes | |\n| `to_number` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoices` | object[] | yes | |\n\n### bkav_lookup_company (workflow step)\n\n- Look up a company in the tax registry by M\xE3 s\u1ED1 thu\u1EBF, through the Bkav account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `tax_code` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `company` | object | yes | |\n| `company.MaSoThue` | string | yes | |\n| `company.TenChinhThuc` | string | yes | |\n| `company.DiaChiGiaoDichChinh` | string | yes | |\n| `company.DiaChiGiaoDichPhu` | string | yes | |\n| `company.TrangThaiHoatDong` | string | yes | |\n| `company.ChuDoanhNghiep` | string | | |\n| `company.SoDienThoai` | string \\\\| null | | |\n| `company.LastUpdate` | string | | |\n\n### bkav_send_lookup_email (workflow step)\n\n- Send an invoice\'s lookup email to the buyer again, or to another email or phone.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guid` | string | yes | |\n| `email` | string | | |\n| `mobile` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `sent` | true | yes | |\n\n### bkav_sign_invoices (workflow step)\n\n- Sign \u2014 issue \u2014 invoices with the account\'s HSM signature, making them tax-registered documents.\n- An account that signs with a USB token cannot sign this way.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `invoice_guids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `signed` | string[] | yes | |\n\n### bkav_submit_invoices (workflow step)\n\n- Create, replace or adjust invoices on Bkav, in Bkav\'s field names.\n- Each carries PartnerInvoiceStringID, your own id for it, which Bkav uses to refuse a duplicate.\n- Attachments are not accepted here.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `kind` | "create_draft" \\\\| "create" \\\\| "create_draft_own_serial" \\\\| "create_own_number" \\\\| "create_own_serial" \\\\| "replace_draft" \\\\| "adjust_draft" \\\\| "adjust_discount_draft" \\\\| "replace" \\\\| "adjust" \\\\| "adjust_external_draft" \\\\| "adjust_discount" \\\\| "adjust_external" \\\\| "replace_external" \\\\| "replace_external_draft" | yes | create_draft: no number, deletable. create: Bkav numbers it. *_own_*: your form/serial. replace_* / adjust_*: each invoice names its original in Invoice.OriginalInvoiceIdentify and gives Invoice.Reason. *_external*: corrects an invoice issued outside Bkav; its first line is an ItemTypeID 4 description of it. *_draft kinds stay unnumbered. |\n| `invoices` | object[] | yes | |\n| `invoices[].Invoice` | object | yes | |\n| `invoices[].Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `invoices[].Invoice.InvoiceDate` | string | yes | ISO date |\n| `invoices[].Invoice.BuyerCode` | string | | |\n| `invoices[].Invoice.BuyerName` | string | | |\n| `invoices[].Invoice.BuyerTaxCode` | string | | |\n| `invoices[].Invoice.BuyerUnitName` | string | | |\n| `invoices[].Invoice.BuyerAddress` | string | | |\n| `invoices[].Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `invoices[].Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `invoices[].Invoice.ReceiverEmail` | string | | |\n| `invoices[].Invoice.ReceiverMobile` | string | | |\n| `invoices[].Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.Note` | string | | |\n| `invoices[].Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.CurrencyID` | string | | |\n| `invoices[].Invoice.ExchangeRate` | number | | |\n| `invoices[].Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `invoices[].Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `invoices[].Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `invoices[].Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `invoices[].Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `invoices[].Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `invoices[].Invoice.isBTH` | boolean | | |\n| `invoices[].Invoice.CCCD` | string | | |\n| `invoices[].Invoice.PassportNumber` | string | | |\n| `invoices[].Invoice.FiscalCodes` | string | | |\n| `invoices[].Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `invoices[].Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `invoices[].Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `invoices[].ListInvoiceDetailsWS` | object[] | yes | |\n| `invoices[].ListInvoiceDetailsWS[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].IsDiscount` | boolean | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemCode` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemName` | string | yes | |\n| `invoices[].ListInvoiceDetailsWS[].UnitName` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].Qty` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Price` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Amount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].TaxRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].OtherAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].UserDefineDetails` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `invoices[].ListInvoiceDetailsWS[].SpecialtyItems` | string | | |\n| `invoices[].PartnerInvoiceStringID` | string | yes | |\n| `invoices[].AttachListNumber` | string | | S\u1ED1 of a b\u1EA3ng k\xEA the invoice refers to |\n| `invoices[].AttachListDate` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n### bkav_update_invoices (workflow step)\n\n- Change invoices not yet issued. by names how each invoice is found: partner (its partner id), number (Invoice.InvoiceForm + InvoiceSerial + InvoiceNo) or guid (Invoice.InvoiceGUID).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Bkav connected account |\n| `by` | "partner" \\\\| "number" \\\\| "guid" | yes | |\n| `invoices` | object[] | yes | |\n| `invoices[].Invoice` | object | yes | |\n| `invoices[].Invoice.InvoiceTypeID` | number | yes | 1 GTGT, 2 b\xE1n h\xE0ng, 5 PXK ki\xEAm VCNB, 6 PXK g\u1EEDi b\xE1n \u0111\u1EA1i l\xFD |\n| `invoices[].Invoice.InvoiceDate` | string | yes | ISO date |\n| `invoices[].Invoice.BuyerCode` | string | | |\n| `invoices[].Invoice.BuyerName` | string | | |\n| `invoices[].Invoice.BuyerTaxCode` | string | | |\n| `invoices[].Invoice.BuyerUnitName` | string | | |\n| `invoices[].Invoice.BuyerAddress` | string | | |\n| `invoices[].Invoice.BuyerBankAccount` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.PayMethodID` | number | | 1 TM, 2 CK, 3 TM/CK, \u2026 |\n| `invoices[].Invoice.ReceiveTypeID` | number | | 1 email, 2 SMS, 3 both, 4 courier |\n| `invoices[].Invoice.ReceiverEmail` | string | | |\n| `invoices[].Invoice.ReceiverMobile` | string | | |\n| `invoices[].Invoice.ReceiverAddress` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.ReceiverName` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.Note` | string | | |\n| `invoices[].Invoice.BillCode` | string | | Sent empty when absent: Bkav refuses it missing |\n| `invoices[].Invoice.CurrencyID` | string | | |\n| `invoices[].Invoice.ExchangeRate` | number | | |\n| `invoices[].Invoice.InvoiceForm` | string | | M\u1EABu s\u1ED1 |\n| `invoices[].Invoice.InvoiceSerial` | string | | K\xFD hi\u1EC7u |\n| `invoices[].Invoice.InvoiceNo` | number | | S\u1ED1; 0 when Bkav assigns it |\n| `invoices[].Invoice.MaCuaCQT` | string | | Set only by a cash-register flow that already holds the authority\'s code |\n| `invoices[].Invoice.UserDefine` | string | | JSON-encoded template fields agreed with Bkav |\n| `invoices[].Invoice.UIDefine` | string | | JSON-encoded delivery-note fields (InvoiceTypeID 5 and 6) |\n| `invoices[].Invoice.isBTH` | boolean | | |\n| `invoices[].Invoice.CCCD` | string | | |\n| `invoices[].Invoice.PassportNumber` | string | | |\n| `invoices[].Invoice.FiscalCodes` | string | | |\n| `invoices[].Invoice.Reason` | string | | Why \u2014 required on a replacement or adjustment |\n| `invoices[].Invoice.OriginalInvoiceIdentify` | string | | `[m\u1EABu s\u1ED1]_[k\xFD hi\u1EC7u]_[s\u1ED1]` of the invoice a replacement or adjustment corrects |\n| `invoices[].Invoice.InvoiceGUID` | string | | Addresses the invoice for an update by guid |\n| `invoices[].ListInvoiceDetailsWS` | object[] | yes | |\n| `invoices[].ListInvoiceDetailsWS[].ItemTypeID` | number | | 0 goods, 4 description line, 15 promotional goods, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].IsDiscount` | boolean | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemCode` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].ItemName` | string | yes | |\n| `invoices[].ListInvoiceDetailsWS[].UnitName` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].Qty` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Price` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].Amount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxRateID` | number | | 1 0%, 2 5%, 3 10%, 4 kh\xF4ng ch\u1ECBu thu\u1EBF, 9 8%, \u2026 |\n| `invoices[].ListInvoiceDetailsWS[].TaxRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].TaxAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].OtherAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountRate` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].DiscountAmount` | number | | |\n| `invoices[].ListInvoiceDetailsWS[].UserDefineDetails` | string | | |\n| `invoices[].ListInvoiceDetailsWS[].IsIncrease` | boolean \\\\| null | | On an adjustment: true raises the amount, false lowers it, absent adjusts information |\n| `invoices[].ListInvoiceDetailsWS[].SpecialtyItems` | string | | |\n| `invoices[].PartnerInvoiceStringID` | string | yes | |\n| `invoices[].AttachListNumber` | string | | S\u1ED1 of a b\u1EA3ng k\xEA the invoice refers to |\n| `invoices[].AttachListDate` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `results` | object[] | yes | |\n| `results[].PartnerInvoiceID` | number | yes | |\n| `results[].PartnerInvoiceStringID` | string \\\\| null | yes | Null on a delete\'s answer |\n| `results[].InvoiceGUID` | string | yes | |\n| `results[].InvoiceForm` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceSerial` | string \\\\| null | yes | Null on a draft\'s link answer |\n| `results[].InvoiceNo` | number | yes | Assigned by a numbered kind; 0 on a draft |\n| `results[].MTC` | string \\\\| null | yes | M\xE3 tra c\u1EE9u the buyer looks the invoice up with; null on a draft\'s link answer |\n| `results[].MaCuaCQT` | string \\\\| null | | |\n| `results[].Status` | number | yes | 0 succeeded; otherwise MessLog says why |\n| `results[].MessLog` | string | yes | |\n| `failed` | number | yes | Invoices whose own Status is not 0 |\n\n## drive\n\n### drive_export_file (workflow step)\n\n- Upload a Lotics file to the member\'s Google Drive.\n- The file is uploaded as-is \u2014 a .docx stays a .docx, a .xlsx stays a .xlsx.\n- It is deliberately NOT converted into a Google Doc or Sheet: conversion re-flows the layout of a generated document, and a native Google file is live, so its figures would drift away from the record they came from.\n- Use this to archive a finished document, the way you would attach it to an email.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `file_id` | string | yes | Lotics file ID to upload |\n| `folder_id` | string | | Drive folder ID to place the file in; omit for the member\'s Drive root |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `drive_file_id` | string | yes | Drive file ID of the uploaded copy |\n| `web_view_link` | string \\\\| null | yes | URL to open the uploaded file in Drive |\n\n### drive_import_file (workflow step)\n\n- Import a Google Drive file into the workspace as a Lotics file, and return its file id.\n- Google Docs arrive as .docx, Sheets as .xlsx, Slides as .pptx; everything else transfers unchanged. **This is also how you READ a Drive file.** There is no separate Drive read tool: import the file, then use `view_files` on the returned id. (workflow step, chat)\n- That path handles scanned documents \u2014 a photographed B/L or a stamped invoice inside a .docx reaches the model as a picture.\n- Exporting Drive content as plain text would return an empty document for exactly those files.\n- Folders, Forms, shortcuts and other Drive items with no file form cannot be imported.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `file_id` | string | yes | Drive file ID, from drive_query_files (workflow step, chat) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | Lotics file ID \u2014 pass this to view_files to read the contents (workflow step, chat) |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n### drive_query_files (workflow step)\n\n- Search the member\'s Google Drive using Drive query syntax. ## Query syntax Terms are `field operator value`; combine with `and` / `or` / `not`.\n- String values are single-quoted. - Name: `name = \'Rate card\'`, `name contains \'invoice\'` - Full text (body + title): `fullText contains \'CMA CGM\'` - Type: `mimeType = \'application/vnd.google-apps.spreadsheet\'`, `mimeType contains \'pdf\'` - Folder: `\'<folder_id>\' in parents` - Time (RFC 3339): `modifiedTime > \'2026-01-01T00:00:00\'` - Exclude deleted: `trashed = false` (always worth adding) - Starred / shared: `starred = true`, `sharedWithMe = true` Google-native types: `application/vnd.google-apps.document` (Docs), `.spreadsheet` (Sheets), `.presentation` (Slides), `.folder`.\n- Examples: - `name contains \'packing list\' and trashed = false` - `\'1a2b3c\' in parents and modifiedTime > \'2026-08-01T00:00:00\'` - `fullText contains \'demurrage\' and mimeType = \'application/vnd.google-apps.document\'` Results carry enough metadata (name, type, modified time, size, owner) to pick a file WITHOUT importing it.\n- Import only the one you actually need \u2014 `drive_import_file` persists a copy in the workspace. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Google Drive account to use |\n| `query` | string | yes | Drive query syntax expression |\n| `max_result` | number | yes | Maximum number of files to retrieve (1-50) |\n| `order_by` | string | | Sort order, e.g. \'modifiedTime desc\', \'name\' |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `files` | object[] | yes | List of matching Drive files |\n| `files[].id` | string | yes | Drive file ID \u2014 pass this to drive_import_file (workflow step, chat) |\n| `files[].name` | string | yes | File name as it appears in Drive |\n| `files[].mime_type` | string | yes | Drive MIME type; \'application/vnd.google-apps.*\' means Google-native |\n| `files[].modified_time` | string \\\\| null | yes | RFC 3339 timestamp of the last modification |\n| `files[].size_bytes` | number \\\\| null | yes | Size in bytes; null for Google-native files, which have no stored size until exported |\n| `files[].owner` | string \\\\| null | yes | Email address of the file\'s owner |\n| `files[].web_view_link` | string \\\\| null | yes | URL to open the file in Drive |\n\n## evaluate\n\n### evaluate_formula (workflow step)\n\n- Compute a formula over one record as its table\'s own formula fields compute: fields referenced as `{fld_\u2026}`, lookups and rollups read as the row holds them.\n- Writes nothing.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `record_id` | string | yes | |\n| `expression` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `value` | number \\\\| string \\\\| boolean \\\\| null | yes | |\n\n## excel\n\n### excel_create_file (workflow step)\n\n- Create an Excel file with styled column headers and auto-filter enabled.\n- Input: { filename, sheets: [{ name, columns: [{ header, width? }], rows: unknown[][] }] } Headers are bold, centered, bordered, with auto-filter on the header row.\n- Best for producing polished report-style spreadsheets.\n- Returns a new file_id \u2014 use it for subsequent operations.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `sheets` | object[] | yes | |\n| `sheets[].name` | string | yes | |\n| `sheets[].columns` | object[] | yes | |\n| `sheets[].columns[].header` | string | yes | |\n| `sheets[].columns[].width` | number | | |\n| `sheets[].rows` | any[][] | yes | 2D array of cell values |\n| `filename` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### excel_find_cells (workflow step)\n\n- Search for cells matching a text query in an Excel worksheet.\n- Input: { file_id, worksheet: string\\|number, query: "search text", match_type?: "contains"\\|"exact"\\|"regex", max_results?: number } Default match_type is "contains" (case-insensitive).\n- Returns matching cell addresses, values, and positions.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `query` | string | yes | Text to find. |\n| `match_type` | "exact" \\\\| "contains" \\\\| "regex" | | How to match: exact, contains (default), or regex |\n| `max_results` | number | | Max matches to return |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `matches` | object[] | yes | |\n| `matches[].cell` | string | yes | Cell address, e.g. B3 |\n| `matches[].value` | string | yes | Cell value as string |\n| `matches[].row` | number | yes | Row number |\n| `matches[].column` | string | yes | Column letter |\n| `total_matches` | number | yes | Total number of matches found |\n\n### excel_format_range (workflow step)\n\n- Apply formatting to a cell range (fonts, fills, borders, alignment, number formats). range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:D10".\n- Never use "B2" alone.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `format` | object | yes | |\n| `format.font_bold` | boolean | | |\n| `format.font_italic` | boolean | | |\n| `format.font_size` | number | | |\n| `format.font_color` | string | | Hex color like #FF0000 |\n| `format.font_name` | string | | |\n| `format.font_underline` | "single" \\\\| "double" | | |\n| `format.font_strike` | boolean | | |\n| `format.background_color` | string | | Hex color like #FFFF00 |\n| `format.horizontal_align` | "left" \\\\| "center" \\\\| "right" | | |\n| `format.vertical_align` | "top" \\\\| "middle" \\\\| "bottom" | | |\n| `format.wrap_text` | boolean | | |\n| `format.number_format` | string | | Excel number format code (e.g. #,##0.00, dd/mm/yyyy) |\n| `format.border_top` | object | | |\n| `format.border_top.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_top.width` | number | | |\n| `format.border_top.color` | string | | |\n| `format.border_bottom` | object | | |\n| `format.border_bottom.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_bottom.width` | number | | |\n| `format.border_bottom.color` | string | | |\n| `format.border_left` | object | | |\n| `format.border_left.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_left.width` | number | | |\n| `format.border_left.color` | string | | |\n| `format.border_right` | object | | |\n| `format.border_right.style` | "solid" \\\\| "dashed" \\\\| "dotted" \\\\| "double" | | |\n| `format.border_right.width` | number | | |\n| `format.border_right.color` | string | | |\n| `column_widths` | object | | Column letter \u2192 width mapping |\n| `row_heights` | object | | Row number \u2192 height mapping |\n| `auto_filter` | string | | Range for an auto-filter (e.g. A1:D1); one over a table turns on that table\'s own filter |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `formatted_cells` | number | yes | |\n\n### excel_get_range (workflow step)\n\n- Read cell values from a range in an Excel worksheet. range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:D10".\n- Never use "B2" alone. worksheet: use sheet name (preferred) or 0-based index.\n- Use view_file first to see available worksheets.\n- Returns data keyed by row number then column letter, e.g. { "1": { "A": "Hello" } }.\n- Maximum 1000 cells per call \u2014 use smaller ranges and paginate if needed.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `options` | object | | Options for data retrieval |\n| `options.include_formulas` | boolean | | Include cell formulas instead of calculated values |\n| `options.include_formatting` | boolean | | Include cell formatting information |\n| `options.max_cell_length` | number | | Max characters returned per cell; longer text is truncated with an ellipsis. Pass -1 to return cells in full \u2014 use when reading free-text columns like notes. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | The worksheet name |\n| `range` | string | yes | The requested range |\n| `data` | object | yes | Cell values keyed by row number then column letter |\n| `formatting` | object | | Cell formatting keyed by row then column |\n| `truncated_cells` | object[] | | Cells with truncated content |\n| `truncated_cells[].cell` | string | yes | Cell address |\n| `truncated_cells[].original_length` | number | yes | Original text length |\n\n### excel_get_rows (workflow step)\n\n- Read every row of a sheet in an .xlsx, .xls or .csv file as a list of objects keyed by the header row\'s text.\n- A cell is its typed value: a number, true or false, text, a date as ISO text ("2026-03-02", "2026-03-02T09:30:00", or "09:30:00" for a time alone), null when blank.\n- A formula cell is the value the file last computed; an error cell is its code as text ("#N/A").\n- Every CSV cell is text or null.\n- A header cell inside a merge takes the merge\'s text; a blank header is keyed by its column letter ("C"), and a header text that repeats gets its column letter added ("Amount (F)"). headers lists the keys in sheet order.\n- Rows above header_row and rows blank in every column are left out.\n- One call reads a sheet of at most 10000 rows, its header counted, and returns at most 250000 cells (rows \xD7 kept columns); a sheet past either is refused with its row count, never cut short.\n- Read-only.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | | Sheet name, or 0-based index. |\n| `header_row` | number | | 1-based row holding the headers. |\n| `columns` | string[] | | Keys to keep, as headers lists them. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | |\n| `header_row` | number | yes | |\n| `headers` | string[] | yes | The key of each returned column, in sheet order |\n| `row_count` | number | yes | |\n| `rows` | object[] | yes | |\n\n### excel_update_range (workflow step)\n\n- Write cell values into a range in an Excel worksheet. range format is always "START:END" \u2014 single cell: "B2:B2", multi-cell: "A1:C3".\n- Never use "B2" alone. values is a 2D array matching the range dimensions.\n- Single cell: [["value"]], row: [["a","b","c"]], grid: [[r1...],[r2...]].\n- Strings starting with "=" are interpreted as formulas (e.g. "=SUM(A1:A10)").\n- Set options.interpret_formulas=false to write literal "=" text.\n- Modifies the document in place.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet` | string \\\\| number | yes | |\n| `range` | string | yes | |\n| `values` | any[][] | yes | 2D array of values to write (rows x columns) |\n| `options` | object | | |\n| `options.preserve_formatting` | boolean | | Preserve existing cell formatting (only update values) |\n| `options.apply_number_format` | string | | Apply a number format to all cells in the range (e.g., \'#,##0.00\') |\n| `options.interpret_formulas` | boolean | | Set false to write \'=\' strings as literal text |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `worksheet_name` | string | yes | |\n| `range` | string | yes | |\n| `updated_cells` | number | yes | Number of cells updated |\n\n## facebook\n\n### facebook_create_post (workflow step)\n\n- Publish a post to a Facebook Page feed.\n- Requires a Facebook connected account whose page allows content management.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | Facebook Page to post to \u2014 one of the connected account\'s managed pages. |\n| `message` | string | yes | |\n| `link` | string | | A post carries either a link or media. |\n| `media_file_ids` | string[] | | Photos, or one video on its own. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n### facebook_list_comments (workflow step)\n\n- Read the most recent comments left on a Facebook Page\'s posts.\n- Reply with facebook_reply_to_comment, passing the comment_id. (workflow step)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | One of the connected account\'s managed pages. |\n| `limit` | integer | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `comments` | object[] | yes | |\n| `comments[].comment_id` | string | yes | |\n| `comments[].post_id` | string | yes | |\n| `comments[].author_id` | string | yes | |\n| `comments[].author_name` | string | yes | |\n| `comments[].message` | string | yes | |\n| `comments[].created_time` | string | yes | |\n\n### facebook_list_conversations (workflow step)\n\n- Read a Facebook Page\'s Messenger conversations with their recent messages.\n- Answer someone with facebook_send_message, passing the person_id. (workflow step)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | One of the connected account\'s managed pages. |\n| `limit` | integer | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `conversations` | object[] | yes | |\n| `conversations[].conversation_id` | string | yes | |\n| `conversations[].person_id` | string \\\\| null | yes | |\n| `conversations[].person_name` | string \\\\| null | yes | |\n| `conversations[].messages` | object[] | yes | |\n| `conversations[].messages[].from_id` | string | yes | |\n| `conversations[].messages[].from_name` | string | yes | |\n| `conversations[].messages[].message` | string | yes | |\n| `conversations[].messages[].created_time` | string | yes | |\n\n### facebook_reply_to_comment (workflow step)\n\n- Reply publicly to a comment on a Facebook Page post, as the page.\n- The reply is visible to everyone who can see the comment.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | The page that owns the post the comment is on. |\n| `comment_id` | string | yes | From facebook_list_comments. (workflow step) |\n| `message` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `comment_id` | string | yes | The reply\'s own id. |\n\n### facebook_send_message (workflow step)\n\n- Answer someone who messaged a Facebook Page, as the page.\n- Meta accepts a reply only within 24 hours of that person\'s last message.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `page_id` | string | yes | The page the person wrote to. |\n| `person_id` | string | yes | From facebook_list_conversations. Page-scoped to this page. (workflow step) |\n| `message` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | |\n\n## fedex\n\n### fedex_cancel_draft (workflow step)\n\n- Cancel (delete) an unconfirmed FedEx draft (Open Ship) booking by its reference.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `reference` | string | yes | The draft\'s reference (Open Ship index) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `cancelled` | boolean | yes | |\n\n### fedex_confirm_shipment (workflow step)\n\n- Confirm a FedEx draft (Open Ship) booking by its reference, generating the label / AWB.\n- Returns the master tracking number and any piece tracking numbers + label URLs.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `reference` | string | yes | The draft\'s reference (Open Ship index) from fedex_create_draft_shipment (workflow step) |\n| `master_tracking_number` | string | | The master_tracking_number returned by fedex_create_draft_shipment \u2014 required to finalize the label (workflow step) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `master_tracking_number` | string | | |\n| `tracking_numbers` | string[] | yes | |\n| `label_urls` | string[] | yes | |\n\n### fedex_create_draft_shipment (workflow step)\n\n- Create a FedEx draft (Open Ship) booking \u2014 no label yet.\n- Confirm later with fedex_confirm_shipment. reference is a unique key you choose (e.g. the shipment order number) used to confirm or cancel. (workflow step)\n- Supports multiple packages, special services, and (international) customs commodities.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `reference` | string | yes | Unique key for this draft (the Open Ship index) |\n| `shipper` | object | yes | |\n| `shipper.contact` | object | | |\n| `shipper.contact.person_name` | string | | |\n| `shipper.contact.company_name` | string | | |\n| `shipper.contact.phone_number` | string | | |\n| `shipper.address` | object | yes | |\n| `shipper.address.postal_code` | string | yes | |\n| `shipper.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.address.city` | string | | |\n| `shipper.address.state_or_province_code` | string | | |\n| `shipper.address.street_lines` | string[] | | |\n| `shipper.address.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.contact` | object | | |\n| `recipient.contact.person_name` | string | | |\n| `recipient.contact.company_name` | string | | |\n| `recipient.contact.phone_number` | string | | |\n| `recipient.address` | object | yes | |\n| `recipient.address.postal_code` | string | yes | |\n| `recipient.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.address.city` | string | | |\n| `recipient.address.state_or_province_code` | string | | |\n| `recipient.address.street_lines` | string[] | | |\n| `recipient.address.residential` | boolean | | |\n| `service_type` | string | yes | FedEx service type e.g. INTERNATIONAL_PRIORITY |\n| `packaging_type` | string | | FedEx packaging type e.g. YOUR_PACKAGING |\n| `packages` | object[] | yes | One entry per package; weight in kg |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `pickup_type` | string | | FedEx pickup type e.g. USE_SCHEDULED_PICKUP |\n| `special_services` | string[] | | FedEx special service types e.g. SATURDAY_DELIVERY, FEDEX_ONE_RATE |\n| `commodities` | object[] | | Customs commodities for international shipments |\n| `commodities[].description` | string | yes | |\n| `commodities[].quantity` | number | yes | |\n| `commodities[].customs_value` | number | yes | |\n| `commodities[].currency` | string | yes | |\n| `commodities[].weight` | number | yes | |\n| `commodities[].weight_units` | "KG" \\\\| "LB" | | |\n| `commodities[].country_of_manufacture` | string | | ISO 3166-1 alpha-2 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `reference` | string | yes | |\n| `status` | "draft" | yes | |\n| `master_tracking_number` | string | | Pass this to fedex_confirm_shipment to finalize the draft (workflow step) |\n\n### fedex_create_shipment (workflow step)\n\n- Create a FedEx shipment and generate the label / AWB in one call.\n- Returns the master tracking number, piece tracking numbers, and label URLs.\n- Use the Open Ship draft flow instead when you need to stage a booking before committing.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `shipper` | object | yes | |\n| `shipper.contact` | object | | |\n| `shipper.contact.person_name` | string | | |\n| `shipper.contact.company_name` | string | | |\n| `shipper.contact.phone_number` | string | | |\n| `shipper.address` | object | yes | |\n| `shipper.address.postal_code` | string | yes | |\n| `shipper.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.address.city` | string | | |\n| `shipper.address.state_or_province_code` | string | | |\n| `shipper.address.street_lines` | string[] | | |\n| `shipper.address.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.contact` | object | | |\n| `recipient.contact.person_name` | string | | |\n| `recipient.contact.company_name` | string | | |\n| `recipient.contact.phone_number` | string | | |\n| `recipient.address` | object | yes | |\n| `recipient.address.postal_code` | string | yes | |\n| `recipient.address.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.address.city` | string | | |\n| `recipient.address.state_or_province_code` | string | | |\n| `recipient.address.street_lines` | string[] | | |\n| `recipient.address.residential` | boolean | | |\n| `service_type` | string | yes | FedEx service type e.g. INTERNATIONAL_PRIORITY |\n| `packaging_type` | string | | FedEx packaging type e.g. YOUR_PACKAGING |\n| `packages` | object[] | yes | One entry per package; weight in kg |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `pickup_type` | string | | FedEx pickup type e.g. USE_SCHEDULED_PICKUP |\n| `special_services` | string[] | | FedEx special service types |\n| `commodities` | object[] | | Customs commodities for international shipments |\n| `commodities[].description` | string | yes | |\n| `commodities[].quantity` | number | yes | |\n| `commodities[].customs_value` | number | yes | |\n| `commodities[].currency` | string | yes | |\n| `commodities[].weight` | number | yes | |\n| `commodities[].weight_units` | "KG" \\\\| "LB" | | |\n| `commodities[].country_of_manufacture` | string | | ISO 3166-1 alpha-2 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `master_tracking_number` | string | | |\n| `tracking_numbers` | string[] | yes | |\n| `label_urls` | string[] | yes | |\n\n### fedex_rate (workflow step)\n\n- Get FedEx rate quotes for a shipment.\n- Returns one priced row per available service (or just the requested service_type).\n- Use before booking to compare costs.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `shipper` | object | yes | |\n| `shipper.postal_code` | string | yes | |\n| `shipper.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.city` | string | | |\n| `shipper.state_or_province_code` | string | | |\n| `shipper.street_lines` | string[] | | |\n| `shipper.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.postal_code` | string | yes | |\n| `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.city` | string | | |\n| `recipient.state_or_province_code` | string | | |\n| `recipient.street_lines` | string[] | | |\n| `recipient.residential` | boolean | | |\n| `packages` | object[] | yes | One entry per package; weight in kg |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `service_type` | string | | FedEx service type e.g. INTERNATIONAL_PRIORITY. Omit to rate all available services. |\n| `packaging_type` | string | | FedEx packaging type e.g. YOUR_PACKAGING |\n| `pickup_type` | string | | FedEx pickup type e.g. USE_SCHEDULED_PICKUP |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `special_services` | string[] | | FedEx special service types e.g. SATURDAY_DELIVERY |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `services` | object[] | yes | |\n| `services[].service_type` | string | yes | |\n| `services[].service_name` | string | | |\n| `services[].total_charge` | number | | |\n| `services[].currency` | string | | |\n\n### fedex_service_availability (workflow step)\n\n- Check what FedEx services serve a lane. query "transit_times" returns delivery commitments per service; "package_and_service_options" returns valid service/packaging combinations; "special_service_options" returns available special services.\n- Use before rating/booking to learn the valid service types for an origin \u2192 destination.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `query` | "transit_times" \\\\| "special_service_options" \\\\| "package_and_service_options" | yes | |\n| `shipper` | object | yes | |\n| `shipper.postal_code` | string | yes | |\n| `shipper.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `shipper.city` | string | | |\n| `shipper.state_or_province_code` | string | | |\n| `shipper.street_lines` | string[] | | |\n| `shipper.residential` | boolean | | |\n| `recipient` | object | yes | |\n| `recipient.postal_code` | string | yes | |\n| `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `recipient.city` | string | | |\n| `recipient.state_or_province_code` | string | | |\n| `recipient.street_lines` | string[] | | |\n| `recipient.residential` | boolean | | |\n| `packages` | object[] | yes | One entry per package; weight in kg unless weight_units set |\n| `packages[].weight` | number | yes | |\n| `packages[].weight_units` | "KG" \\\\| "LB" | | Weight units; FedEx requires LB for US domestic express |\n| `packages[].length` | number | | |\n| `packages[].width` | number | | |\n| `packages[].height` | number | | |\n| `packages[].dimension_units` | "CM" \\\\| "IN" | | |\n| `ship_datestamp` | string | | Ship date YYYY-MM-DD |\n| `carrier_codes` | string[] | yes | FedEx carrier codes \u2014 at least one, e.g. FDXE (Express), FDXG (Ground) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `result` | object | yes | |\n\n### fedex_validate_address (workflow step)\n\n- Validate one or more addresses with FedEx.\n- Returns each resolved address plus its classification (e.g.\n- BUSINESS / RESIDENTIAL) and deliverability attributes.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | FedEx connected account |\n| `addresses` | object[] | yes | |\n| `addresses[].postal_code` | string | yes | |\n| `addresses[].country_code` | string | yes | ISO 3166-1 alpha-2 country code, e.g. VN, US, SG |\n| `addresses[].city` | string | | |\n| `addresses[].state_or_province_code` | string | | |\n| `addresses[].street_lines` | string[] | | |\n| `addresses[].residential` | boolean | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `resolved_addresses` | object[] | yes | |\n| `resolved_addresses[].city` | string | | |\n| `resolved_addresses[].state_or_province_code` | string | | |\n| `resolved_addresses[].postal_code` | string | | |\n| `resolved_addresses[].country_code` | string | | |\n| `resolved_addresses[].classification` | string | | |\n| `resolved_addresses[].attributes` | object | | |\n\n## generate\n\n### generate_bank_qr_code (workflow step)\n\n- Generate a bank-payment QR code (VietQR for Vietnam) and save it as a PNG to the workspace.\n- The QR encodes the recipient bank, account number, and optional amount/description so any banking app can scan and pre-fill the transfer.\n- Call list_banks first to discover valid bank codes. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `country` | "VN" | yes | Country whose bank-payment QR standard to use. Currently only \'VN\' (Vietnam / VietQR) is supported. |\n| `bank_code` | "ICB" \\\\| "VCB" \\\\| "BIDV" \\\\| "VBA" \\\\| "OCB" \\\\| "MB" \\\\| "TCB" \\\\| "ACB" \\\\| "VPB" \\\\| "TPB" \\\\| "STB" \\\\| "HDB" \\\\| "VCCB" \\\\| "SCB" \\\\| "VIB" \\\\| "SHB" \\\\| "EIB" \\\\| "MSB" \\\\| "CAKE" \\\\| "Ubank" \\\\| "TIMO" \\\\| "SGICB" \\\\| "BAB" \\\\| "momo" \\\\| "PVDB" \\\\| "PVCB" \\\\| "MBV" \\\\| "NCB" \\\\| "SHBVN" \\\\| "ABB" \\\\| "VAB" \\\\| "NAB" \\\\| "PGB" \\\\| "VIETBANK" \\\\| "BVB" \\\\| "SEAB" \\\\| "COOPBANK" \\\\| "LPB" \\\\| "KLB" \\\\| "KBank" \\\\| "CIMB" \\\\| "WVN" | yes | Bank code from list_banks. Must belong to the specified country. (workflow step, chat) |\n| `account_number` | string | yes | Beneficiary account number at the bank. |\n| `amount` | number | | Payment amount in the local currency (VND for Vietnam). Omit for a static QR with no fixed amount. |\n| `description` | string | | Transfer description / memo. Max 50 bytes; Vietnamese diacritics use 2\u20133 bytes per character. |\n| `account_name` | string | | Beneficiary name shown on the payer\'s banking app confirmation screen. Max 25 bytes \u2014 banks normalize Vietnamese names to ASCII uppercase, pass the normalized form (e.g. \'NGUYEN VAN A\'). |\n| `filename` | string | | Output filename. Defaults to \'bank_qr_<bank_code>.png\'. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n| `bank` | object | yes | |\n| `bank.country` | string | yes | |\n| `bank.code` | string | yes | |\n| `bank.short_name` | string | yes | |\n| `bank.full_name` | string | yes | |\n\n### generate_image (workflow step)\n\n- Generate an image from a text prompt and save it to the workspace.\n- Returns a file_id usable as an image field value or attachment.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `prompt` | string | yes | Description of the image to generate. |\n| `size` | "1024x1024" \\\\| "1024x1536" \\\\| "1536x1024" | | Square, portrait, or landscape. |\n| `quality` | "low" \\\\| "medium" \\\\| "high" | | Higher quality costs more. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `url` | string | yes | |\n| `width` | number | yes | |\n| `height` | number | yes | |\n| `mime_type` | string | yes | |\n\n### generate_qr_code (workflow step)\n\n- Generate a QR code PNG from text or a URL and save it to the workspace.\n- For bank payment QR codes, use generate_bank_qr_code instead \u2014 it builds the correct EMVCo payload from a bank code and account number. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `data` | string | yes | Text or URL to encode into the QR code. |\n| `filename` | string | | Output filename. Defaults to \'qr_code.png\'. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n## gmail\n\n### gmail_get_thread (workflow step)\n\n- Retrieve all messages in a Gmail thread.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Gmail account to use |\n| `thread_id` | string | yes | Gmail thread ID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `messages` | object[] | yes | |\n| `messages[].id` | string | yes | Gmail message ID |\n| `messages[].threadId` | string | yes | Gmail thread ID |\n| `messages[].labelIds` | string[] | yes | Gmail label IDs (e.g. INBOX, UNREAD) |\n| `messages[].subject` | string | yes | Email subject |\n| `messages[].from` | string | yes | Sender email address |\n| `messages[].to` | string | yes | Recipient email address |\n| `messages[].date` | string | yes | Email date in the workspace timezone, with its offset |\n| `messages[].snippet` | string | yes | Short plain-text preview of the email body |\n| `messages[].cc` | string \\\\| null | | CC recipients |\n| `messages[].bcc` | string \\\\| null | | BCC recipients |\n\n### gmail_query_emails (workflow step)\n\n- Search Gmail messages using Gmail search syntax. ## Query syntax Operators (no space after colon): - People: from:, to:, cc:, bcc:, deliveredto: - Content: subject:, "exact phrase", +exact_word, AROUND N (proximity) - Status: is:unread, is:read, is:starred, is:important, is:muted - Labels: label:name, in:inbox, in:sent, in:drafts, in:spam, in:trash, in:anywhere - Category: category:primary, category:social, category:promotions, category:updates, category:forums - Attachments: has:attachment, has:drive, filename:pdf, filename:report.xlsx (has:attachment matches non-inline attachments only \u2014 it misses inline-embedded PDFs, common in receipts/invoices; open the message with gmail_read_email to detect those) - Size: larger:5M, smaller:2M, size:1000000 - Dates (YYYY/MM/DD): after:2024/01/01, before:2024/12/31 - Relative dates: newer_than:7d, older_than:3m, newer_than:1y (d=days, m=months, y=years) Combining: space = AND, OR (uppercase), - (exclude), () for grouping, {} alternative OR syntax. (workflow step, chat)\n- Examples: - from:alice is:unread has:attachment - subject:"quarterly review" newer_than:2d - {from:alice from:bob} subject:project -in:drafts - larger:5M filename:pdf older_than:6m - in:anywhere deliveredto:billing@company.com\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Gmail account to use |\n| `query` | string | yes | Gmail search syntax expression |\n| `max_result` | number | yes | Maximum number of emails to retrieve (1-50) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `messages` | object[] | yes | List of matching Gmail messages |\n| `messages[].id` | string | yes | Gmail message ID |\n| `messages[].threadId` | string | yes | Gmail thread ID |\n| `messages[].labelIds` | string[] | yes | Gmail label IDs (e.g. INBOX, UNREAD) |\n| `messages[].subject` | string | yes | Email subject |\n| `messages[].from` | string | yes | Sender email address |\n| `messages[].to` | string | yes | Recipient email address |\n| `messages[].date` | string | yes | Email date in the workspace timezone, with its offset |\n| `messages[].snippet` | string | yes | Short plain-text preview of the email body |\n| `messages[].cc` | string \\\\| null | | CC recipients |\n| `messages[].bcc` | string \\\\| null | | BCC recipients |\n\n### gmail_query_labels (workflow step)\n\n- List Gmail labels with message and thread counts.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected Gmail account to use |\n| `name` | string | | Match labels by name prefix. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `labels` | object[] | yes | |\n| `labels[].id` | string | yes | |\n| `labels[].name` | string | yes | |\n| `labels[].type` | string | yes | system or user |\n| `labels[].messagesTotal` | number | yes | |\n| `labels[].messagesUnread` | number | yes | |\n| `labels[].threadsTotal` | number | yes | |\n| `labels[].threadsUnread` | number | yes | |\n\n### gmail_read_email (workflow step)\n\n- Fetch a Gmail message\'s body and the list of its attachments.\n- Attachments are stored as workspace files \u2014 the result names each one\'s file_id; read one with view_files, attach one to a record with update_records. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The ID of the connected account |\n| `message_id` | string | yes | The ID of the Gmail message to retrieve |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message` | object | yes | The full Gmail message |\n| `message.id` | string | | Gmail message ID |\n| `message.threadId` | string | | Gmail thread ID |\n| `message.historyId` | string | | Gmail history ID |\n| `message.labelIds` | string[] | | Gmail label IDs |\n| `message.subject` | string | yes | Email subject |\n| `message.from` | string | yes | Sender email address |\n| `message.to` | string | yes | Recipient email address |\n| `message.date` | string | yes | Email date in the workspace timezone, with its offset |\n| `message.body` | object | yes | Email body |\n| `message.body.type` | "text" \\\\| "html" | yes | Body content type |\n| `message.body.content` | string | yes | Body content |\n| `message.cc` | string \\\\| null | | CC recipients |\n| `message.bcc` | string \\\\| null | | BCC recipients |\n| `message.inReplyTo` | string \\\\| null | | In-Reply-To header |\n| `message.replyTo` | string \\\\| null | | Reply-To header |\n| `message.references` | string[] \\\\| null | | References header |\n| `message.attachments` | object[] | yes | Email attachments |\n| `message.attachments[].partId` | string | yes | MIME part ID |\n| `message.attachments[].filename` | string | yes | Attachment filename as stored, which may differ from the sender\'s |\n| `message.attachments[].mimeType` | string | yes | MIME type as stored, recovered from the filename when the sender declared a generic one |\n| `message.attachments[].attachmentId` | string | yes | Gmail attachment ID |\n| `message.attachments[].file_id` | string | | Stored file ID \u2014 pass to view_files to read it, or to update_records to attach it (workflow step, chat) |\n| `message.attachments[].size` | number | | Stored size in bytes |\n\n### gmail_reply_email (workflow step)\n\n- Draft or send a reply to a Gmail message.\n- Use reply_body (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `original_message_id` | string | yes | |\n| `reply_body` | string | | Reply body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string | yes | The original sender the reply went to |\n\n### gmail_send_email (workflow step)\n\n- Draft or send a Gmail email.\n- Use content (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `to` | string | | Recipient email(s), comma-separated. Required unless using a template. |\n| `subject` | string | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `content` | string | | Email body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string | yes | The recipients as sent, comma-separated |\n\n## instagram\n\n### instagram_create_post (workflow step)\n\n- Publish to the connected Instagram account.\n- Instagram has no text-only posts.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `caption` | string | yes | |\n| `media_file_ids` | string[] | yes | One image, one video (published as a reel), or 2\u201310 images and videos as a carousel. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## kiotviet\n\n### kiotviet_cancel_invoice (workflow step)\n\n- Cancel a KiotViet sales invoice by id, returning its stock.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `void_payment` | boolean | | Also void payments recorded on the invoice |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `cancelled` | true | yes | |\n\n### kiotviet_cancel_order (workflow step)\n\n- Cancel a KiotViet sales order by id, releasing the stock it reserved.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `void_payment` | boolean | | Also void payments recorded on the order |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `cancelled` | true | yes | |\n\n### kiotviet_create_category (workflow step)\n\n- Create a KiotViet product group (nh\xF3m h\xE0ng), optionally under a parent group.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `name` | string | yes | |\n| `parent_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `category` | object | yes | |\n\n### kiotviet_create_customer (workflow step)\n\n- Create a KiotViet customer (kh\xE1ch h\xE0ng).\n- Returns the customer as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `name` | string | yes | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `contact_number` | string | | |\n| `email` | string | | |\n| `address` | string | | |\n| `location_name` | string | | Province/district as KiotViet names it |\n| `ward_name` | string | | |\n| `gender` | boolean | | true for male, false for female |\n| `birth_date` | string | | yyyy-mm-dd |\n| `group_ids` | number[] | | Customer group ids |\n| `comments` | string | | |\n| `branch_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `customer` | object | yes | |\n\n### kiotviet_create_invoice (workflow step)\n\n- Create a KiotViet sales invoice (h\xF3a \u0111\u01A1n), which deducts stock.\n- Pass order_id to invoice an existing order.\n- Returns the invoice as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `branch_id` | number | yes | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | yes | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | yes | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n| `order_id` | number | | The sales order this invoice settles |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### kiotviet_create_order (workflow step)\n\n- Create a KiotViet sales order (\u0111\u1EB7t h\xE0ng) \u2014 the pre-invoice stage, which reserves stock.\n- Returns the order as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `branch_id` | number | yes | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | yes | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n| `make_invoice` | boolean | | Also issue the invoice at once |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `order` | object | yes | |\n\n### kiotviet_create_product (workflow step)\n\n- Create a KiotViet product (h\xE0ng h\xF3a), optionally with opening stock per branch.\n- Returns the product as KiotViet stores it, with its id and code.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `name` | string | yes | |\n| `code` | string | | Product code (m\xE3 h\xE0ng) |\n| `category_id` | number | yes | Product group id |\n| `base_price` | number | | Selling price |\n| `unit` | string | | |\n| `allows_sale` | boolean | | |\n| `is_active` | boolean | | |\n| `description` | string | | |\n| `weight` | number | | Grams |\n| `inventories` | object[] | | Stock per branch |\n| `inventories[].branch_id` | number | yes | |\n| `inventories[].on_hand` | number | yes | |\n| `inventories[].cost` | number | | Unit cost at this branch |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `product` | object | yes | |\n\n### kiotviet_delete_product (workflow step)\n\n- Delete a KiotViet product by id.\n- KiotViet refuses one that already appears on invoices.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `deleted` | true | yes | |\n\n### kiotviet_get_customer (workflow step)\n\n- Get one KiotViet customer by id or by customer code (m\xE3 kh\xE1ch h\xE0ng).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng), e.g. KH000123 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `customer` | object | yes | |\n\n### kiotviet_get_invoice (workflow step)\n\n- Get one KiotViet sales invoice by id or by invoice code (m\xE3 h\xF3a \u0111\u01A1n), with its line items and payments.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | | |\n| `code` | string | | Invoice code (m\xE3 h\xF3a \u0111\u01A1n), e.g. HD000123 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### kiotviet_get_product (workflow step)\n\n- Get one KiotViet product by id or by product code (m\xE3 h\xE0ng), with stock per branch in inventories[].\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | | |\n| `code` | string | | Product code (m\xE3 h\xE0ng), e.g. SP000123 |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `product` | object | yes | |\n\n### kiotviet_list_branches (workflow step)\n\n- List the store\'s KiotViet branches (chi nh\xE1nh).\n- Branch ids scope inventory, invoices and orders.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_categories (workflow step)\n\n- List KiotViet product groups (nh\xF3m h\xE0ng).\n- Each row carries its parentId, so the tree can be rebuilt from the flat list.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_customers (workflow step)\n\n- List KiotViet customers (kh\xE1ch h\xE0ng) with their lifetime totals \u2014 totalInvoiced, totalRevenue, debt, totalPoint.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `name` | string | | Name search |\n| `contact_number` | string | | Phone number |\n| `group_id` | number | | Only customers in this customer group |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_invoices (workflow step)\n\n- List KiotViet sales invoices (h\xF3a \u0111\u01A1n) with their line items in invoiceDetails[] \u2014 productCode, productName, quantity, price, discount, subTotal.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `branch_ids` | number[] | | |\n| `customer_ids` | number[] | | |\n| `customer_code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `status` | number[] | | KiotViet status codes; each row carries statusValue with the label |\n| `from_purchase_date` | string | | Date, yyyy-mm-dd |\n| `to_purchase_date` | string | | Date, yyyy-mm-dd |\n| `include_payment` | boolean | | Include payments[] on each row |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_orders (workflow step)\n\n- List KiotViet sales orders (\u0111\u1EB7t h\xE0ng) \u2014 the pre-invoice stage \u2014 with line items in orderDetails[].\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `branch_ids` | number[] | | |\n| `customer_ids` | number[] | | |\n| `customer_code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `status` | number[] | | KiotViet status codes; each row carries statusValue with the label |\n| `from_purchase_date` | string | | Date, yyyy-mm-dd |\n| `to_purchase_date` | string | | Date, yyyy-mm-dd |\n| `include_payment` | boolean | | Include payments[] on each row |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_products (workflow step)\n\n- List KiotViet products (h\xE0ng h\xF3a) with stock per branch.\n- Each row\'s inventories[] carries branchId, onHand (t\u1ED3n kho), reserved (\u0111\u1EB7t h\xE0ng) and cost.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `category_id` | number | | Only products in this product group |\n| `is_active` | boolean | | Only products still on sale (true) or discontinued (false) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_list_purchase_orders (workflow step)\n\n- List KiotViet purchase orders (phi\u1EBFu nh\u1EADp h\xE0ng) \u2014 goods received from suppliers \u2014 with line items in purchaseOrderDetails[].\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `page_size` | number | | Rows per page, up to 100 |\n| `current_item` | number | | Zero-based offset of the first row |\n| `last_modified_from` | string | | ISO datetime; only rows modified at or after it |\n| `include_removed_ids` | boolean | | Also return removed_ids \u2014 rows deleted since last_modified_from |\n| `order_by` | string | | KiotViet field name to sort by |\n| `order_direction` | "Asc" \\\\| "Desc" | | |\n| `branch_ids` | number[] | | |\n| `status` | number[] | | KiotViet status codes; each row carries statusValue with the label |\n| `from_purchase_date` | string | | Date, yyyy-mm-dd |\n| `to_purchase_date` | string | | Date, yyyy-mm-dd |\n| `include_payment` | boolean | | Include payments[] on each row |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `total` | number | | Rows matching the query across all pages |\n| `page_size` | number | | |\n| `items` | object[] | yes | Rows as KiotViet returns them |\n| `removed_ids` | number[] | | |\n\n### kiotviet_update_category (workflow step)\n\n- Rename a KiotViet product group or move it under another parent.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `name` | string | | |\n| `parent_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `category` | object | yes | |\n\n### kiotviet_update_customer (workflow step)\n\n- Update a KiotViet customer by id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `name` | string | | |\n| `code` | string | | Customer code (m\xE3 kh\xE1ch h\xE0ng) |\n| `contact_number` | string | | |\n| `email` | string | | |\n| `address` | string | | |\n| `location_name` | string | | Province/district as KiotViet names it |\n| `ward_name` | string | | |\n| `gender` | boolean | | true for male, false for female |\n| `birth_date` | string | | yyyy-mm-dd |\n| `group_ids` | number[] | | Customer group ids |\n| `comments` | string | | |\n| `branch_id` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `customer` | object | yes | |\n\n### kiotviet_update_invoice (workflow step)\n\n- Update a KiotViet sales invoice by id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `branch_id` | number | | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invoice` | object | yes | |\n\n### kiotviet_update_order (workflow step)\n\n- Update a KiotViet sales order by id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `branch_id` | number | | |\n| `customer_id` | number | | |\n| `sold_by_id` | number | | KiotViet user id of the seller |\n| `purchase_date` | string | | ISO datetime |\n| `description` | string | | |\n| `discount` | number | | Discount amount on the whole document |\n| `lines` | object[] | | |\n| `lines[].product_id` | number | yes | |\n| `lines[].quantity` | number | yes | |\n| `lines[].price` | number | | Unit price |\n| `lines[].discount` | number | | Discount amount on the line |\n| `lines[].note` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `order` | object | yes | |\n\n### kiotviet_update_product (workflow step)\n\n- Update a KiotViet product by id; inventories set the stock level per branch.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | KiotViet connected account |\n| `id` | number | yes | |\n| `name` | string | | |\n| `code` | string | | Product code (m\xE3 h\xE0ng) |\n| `category_id` | number | | Product group id |\n| `base_price` | number | | Selling price |\n| `unit` | string | | |\n| `allows_sale` | boolean | | |\n| `is_active` | boolean | | |\n| `description` | string | | |\n| `weight` | number | | Grams |\n| `inventories` | object[] | | Stock per branch |\n| `inventories[].branch_id` | number | yes | |\n| `inventories[].on_hand` | number | yes | |\n| `inventories[].cost` | number | | Unit cost at this branch |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `product` | object | yes | |\n\n## lark\n\n### lark_create_records (workflow step)\n\n- Create one or many records in a Lark Base (Bitable) table (max 500 per call). fields are keyed by Lark field name.\n- Returns the new record ids.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `records` | object[] | yes | |\n| `records[].fields` | object | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `created` | number | yes | |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n\n### lark_delete_records (workflow step)\n\n- Delete one or many records from a Lark Base (Bitable) table by record_id (max 500 per call).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `record_ids` | string[] | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `deleted` | number | yes | |\n| `record_ids` | string[] | yes | |\n\n### lark_get_record (workflow step)\n\n- Get one record from a Lark Base (Bitable) table by its record_id.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `record_id` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `record_id` | string | yes | |\n| `fields` | object | yes | |\n\n### lark_list_fields (workflow step)\n\n- List the fields (columns) of a Lark Base (Bitable) table \u2014 name, id, and type.\n- Use to discover the schema before reading or writing records.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `page_size` | number | | |\n| `page_token` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `fields` | object[] | yes | |\n| `fields[].field_id` | string | | |\n| `fields[].field_name` | string | | |\n| `fields[].type` | number | | |\n| `fields[].ui_type` | string | | |\n| `fields[].is_primary` | boolean | | |\n| `has_more` | boolean | yes | |\n| `page_token` | string | | |\n\n### lark_list_records (workflow step)\n\n- List records from a Lark Base (Bitable) table.\n- Returns each record\'s id and fields (keyed by Lark field name).\n- Optionally narrow with filter / sort / field_names / view_id; paginate with page_token.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `filter` | object | | |\n| `filter.conjunction` | "and" \\\\| "or" | yes | |\n| `filter.conditions` | object[] | yes | |\n| `filter.conditions[].field_name` | string | yes | |\n| `filter.conditions[].operator` | string | yes | e.g. is, isNot, contains, isGreater, isEmpty |\n| `filter.conditions[].value` | string[] | | |\n| `sort` | object[] | | |\n| `sort[].field_name` | string | yes | |\n| `sort[].desc` | boolean | | |\n| `field_names` | string[] | | Restrict returned fields by name |\n| `view_id` | string | | |\n| `page_size` | number | | |\n| `page_token` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n| `has_more` | boolean | yes | |\n| `page_token` | string | | |\n| `total` | number | | |\n\n### lark_list_tables (workflow step)\n\n- List the tables in a Lark Base (Bitable) \u2014 id and name.\n- Use to discover which table_id to read or write.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `page_size` | number | | |\n| `page_token` | string | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `tables` | object[] | yes | |\n| `tables[].table_id` | string | | |\n| `tables[].name` | string | | |\n| `has_more` | boolean | yes | |\n| `page_token` | string | | |\n\n### lark_update_records (workflow step)\n\n- Update one or many existing records in a Lark Base (Bitable) table by record_id (max 500 per call). fields are keyed by Lark field name; only provided fields change.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Lark connected account |\n| `app_token` | string | yes | Lark Base app_token (the Base id) |\n| `table_id` | string | yes | Lark Bitable table id |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `updated` | number | yes | |\n| `records` | object[] | yes | |\n| `records[].record_id` | string | yes | |\n| `records[].fields` | object | yes | |\n\n## linkedin\n\n### linkedin_create_post (workflow step)\n\n- Publish a post to the connected LinkedIn member\'s profile.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `text` | string | yes | |\n| `link` | string | | Shown as an article card; needs link_title. A post carries either a link or media. |\n| `link_title` | string | | |\n| `media_file_ids` | string[] | | Up to 20 images, or one video. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## list\n\n### list_banks (workflow step)\n\n- List banks supported by generate_bank_qr_code. (workflow step, chat)\n- Returns the bank code (used as input to the generator), display name, and country.\n- Call this before generate_bank_qr_code to discover valid codes. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `country` | "VN" | | Filter to banks from a specific country. Omit to list all supported banks. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `banks` | object[] | yes | |\n| `banks[].country` | string | yes | |\n| `banks[].code` | string | yes | |\n| `banks[].bin` | string | yes | |\n| `banks[].short_name` | string | yes | |\n| `banks[].full_name` | string | yes | |\n\n## lookup\n\n### lookup_business (workflow step)\n\n- Look up business registration info by tax / registration code.\n- Defaults to Vietnam (country=\'VN\').\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `tax_code` | string | yes | Business tax / registration code. Format varies by country; for VN this is a 10\u201313 digit m\xE3 s\u1ED1 thu\u1EBF. |\n| `country` | string | | ISO country code. Defaults to \'VN\' (Vietnam). Only \'VN\' is supported today. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `country` | string | yes | |\n| `tax_code` | string | yes | |\n| `name` | string | yes | |\n| `address` | string | yes | |\n| `representative` | string \\\\| null | yes | |\n| `status` | string \\\\| null | yes | |\n| `international_name` | string \\\\| null | yes | |\n| `short_name` | string \\\\| null | yes | |\n| `operating_since` | string \\\\| null | yes | |\n| `managed_by` | string \\\\| null | yes | |\n| `company_type` | string \\\\| null | yes | |\n| `primary_industry` | string \\\\| null | yes | |\n\n## misa\n\n### misa_get_dictionary (workflow step)\n\n- Read master-data records from MISA AMIS to look up codes (customer code, item code, warehouse code, etc.) before pushing a voucher.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | MISA connected account to read from |\n| `data_type` | 1 \\\\| 2 \\\\| 3 \\\\| 4 \\\\| 5 \\\\| 6 \\\\| 8 \\\\| 9 \\\\| 10 \\\\| 11 \\\\| 12 | yes | 1 objects (customers/suppliers/employees), 2 inventory items, 3 stocks, 4 units, 5 chart of accounts, 6 organization units, 8 bank accounts, 9 projects, 10 cost objects, 11 payment terms, 12 banks |\n| `skip` | integer | | |\n| `take` | integer | | Max 100 per page |\n| `last_sync_time` | string | | ISO timestamp \u2014 only return records changed since this time |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `success` | boolean | yes | |\n| `data` | any | yes | Raw dictionary records as returned by MISA |\n| `last_sync_time` | string | | Cursor \u2014 pass as last_sync_time on the next call to fetch only newer changes |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_bank_voucher (workflow step)\n\n- Push a bank voucher (deposit / withdrawal / transfer) into MISA AMIS. voucher_type: 1 deposit (b\xE1o c\xF3), 3 withdrawal/debit-advice (b\xE1o n\u1EE3), 2 internal transfer. bank_account_id is the company-side bank account GUID \u2014 look it up with misa_get_dictionary (data_type 8). (workflow step)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 1 \\\\| 2 \\\\| 3 | yes | |\n| `reftype` | integer | | |\n| `org_refid` | string | yes | Caller-controlled UUID identifying the source document |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `bank_account_id` | string | yes | Bank account GUID in MISA \u2014 look up via misa_get_dictionary (workflow step) |\n| `account_object_code` | string | | |\n| `account_object_name` | string | | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `detail` | object[] | yes | |\n| `detail[].description` | string | yes | |\n| `detail[].amount_oc` | number | yes | |\n| `detail[].amount` | number | yes | |\n| `detail[].debit_account` | string | yes | |\n| `detail[].credit_account` | string | yes | |\n| `detail[].sort_order` | integer | | |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_cash_voucher (workflow step)\n\n- Push a cash voucher (cash receipt or cash payment) into MISA AMIS. voucher_type: 5 receipt (thu ti\u1EC1n), 4 payment (chi ti\u1EC1n). detail array carries the journal lines with debit_account/credit_account per Vietnamese chart-of-accounts.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 4 \\\\| 5 | yes | |\n| `reftype` | integer | | |\n| `org_refid` | string | yes | Caller-controlled UUID identifying the source document |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `account_object_code` | string | | Counterparty code (customer/supplier/employee) |\n| `account_object_name` | string | | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `detail` | object[] | yes | |\n| `detail[].description` | string | yes | |\n| `detail[].amount_oc` | number | yes | |\n| `detail[].amount` | number | yes | |\n| `detail[].debit_account` | string | yes | |\n| `detail[].credit_account` | string | yes | |\n| `detail[].sort_order` | integer | | |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_dictionary (workflow step)\n\n- Create or update master-data records in MISA AMIS (customers, suppliers, items, warehouses, etc.).\n- Use this before pushing a voucher that references a code MISA doesn\'t yet know about.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n| `dictionary_type` | 1 \\\\| 2 \\\\| 3 \\\\| 4 \\\\| 5 \\\\| 6 \\\\| 7 \\\\| 8 \\\\| 9 \\\\| 10 \\\\| 12 | yes | 1 customers/suppliers/employees, 2 object groups, 3 inventory items, 4 item categories, 5 warehouses, 6 units, 7 bank accounts, 8 banks, 9 expense items, 10 revenue/expense categories, 12 cost allocation |\n| `items` | object[] | yes | Master-data records to create or update. 1\u2013200 per call. Each item\'s required fields depend on dictionary_type \u2014 see MISA Open API docs (chat, MCP, CLI) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `count` | integer | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_purchase_voucher (workflow step)\n\n- Push a purchase voucher (purchase invoice / return / discount-debit-note-to-supplier / service / general purchase / purchase order) into MISA AMIS. voucher_type: 15 invoice, 18 voucher, 17 service, 16 return, 14 discount (debit note to supplier), 21 purchase order. account_object_code identifies the supplier in MISA \u2014 must already exist.\n- MISA sync ack only confirms queueing; final accept/reject arrives async.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 14 \\\\| 15 \\\\| 16 \\\\| 17 \\\\| 18 \\\\| 21 | yes | |\n| `reftype` | integer | | |\n| `org_refid` | string | yes | Caller-controlled UUID identifying the source document |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `account_object_code` | string | yes | Supplier code in MISA |\n| `account_object_name` | string | | |\n| `account_object_tax_code` | string | | |\n| `account_object_address` | string | | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `total_vat_amount_oc` | number | yes | |\n| `total_vat_amount` | number | yes | |\n| `detail` | object[] | yes | |\n| `detail[].inventory_item_code` | string | | Item/product code in MISA. Skip when is_description=true |\n| `detail[].inventory_item_name` | string | yes | Item/product display name |\n| `detail[].inventory_item_type` | 0 \\\\| 1 \\\\| 2 \\\\| 3 | | Item type \u2014 0 goods, 1 finished, 2 service, 3 raw material |\n| `detail[].description` | string | | Line description |\n| `detail[].is_description` | boolean | | Description-only line \u2014 when true, only `description` is required |\n| `detail[].stock_code` | string | | Warehouse code in MISA |\n| `detail[].unit_name` | string | | Unit of measure name |\n| `detail[].quantity` | number | yes | Line quantity |\n| `detail[].unit_price` | number | yes | Unit price (in currency_id of voucher) |\n| `detail[].amount_oc` | number | yes | Line total in original currency |\n| `detail[].amount` | number | yes | Line total in accounting currency (= amount_oc when VND) |\n| `detail[].discount_rate` | number | | Line discount percent |\n| `detail[].discount_amount_oc` | number | | Discount amount, original currency |\n| `detail[].discount_amount` | number | | Discount amount, accounting currency |\n| `detail[].vat_rate` | number | yes | VAT percent. 0/5/8/10 for standard, -1 exempt, -2 not creditable, -3 other (set other_vat_rate) |\n| `detail[].other_vat_rate` | number | | Custom VAT percent \u2014 required when vat_rate=-3 |\n| `detail[].vat_amount_oc` | number | yes | VAT amount, original currency |\n| `detail[].vat_amount` | number | yes | VAT amount, accounting currency |\n| `detail[].debit_account` | string | yes | Chart-of-accounts code on debit side, e.g. 131 |\n| `detail[].credit_account` | string | yes | Chart-of-accounts code on credit side, e.g. 511 |\n| `detail[].sale_account` | string | | |\n| `detail[].vat_account` | string | | |\n| `detail[].inventory_account` | string | | |\n| `detail[].sort_order` | integer | | |\n| `detail[].lot_no` | string | | |\n| `detail[].expiry_date` | string | | ISO date |\n| `detail[].organization_unit_code` | string | | Department/branch code |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n### misa_save_sales_voucher (workflow step)\n\n- Push a sales voucher (invoice / sales return / discount-credit-note / sales order) into MISA AMIS. voucher_type: 11 invoice, 13 sales voucher, 12 return, 10 discount/credit note, 20 sales order. reftype refines the sub-type (e.g. 3530 unpaid sale, 3560 sales invoice, 3571 service cash). account_object_code identifies the customer in MISA \u2014 must already exist (use misa_save_dictionary first if not). (workflow step)\n- MISA sync ack only confirms queueing; final accept/reject arrives async on the registered callback URL.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `voucher_type` | 10 \\\\| 11 \\\\| 12 \\\\| 13 \\\\| 20 | yes | |\n| `reftype` | integer | | Voucher sub-type code \u2014 MISA assigns a default when omitted |\n| `org_refid` | string | yes | Caller-controlled UUID identifying the source document |\n| `org_refno` | string | | Source-system document number |\n| `refdate` | string | yes | Document date, ISO 8601 |\n| `posted_date` | string | yes | Accounting posting date, ISO 8601 |\n| `branch_id` | string | | MISA branch GUID, optional for single-branch tenants |\n| `currency_id` | string | | Currency code, default VND |\n| `exchange_rate` | number | | FX rate, default 1.0 |\n| `journal_memo` | string | | Voucher memo/notes |\n| `custom_field1` | string | | |\n| `custom_field2` | string | | |\n| `custom_field3` | string | | |\n| `custom_field4` | string | | |\n| `custom_field5` | string | | |\n| `account_object_code` | string | yes | Customer code in MISA |\n| `account_object_name` | string | | |\n| `account_object_tax_code` | string | | |\n| `account_object_address` | string | | |\n| `employee_code` | string | | Salesperson code |\n| `employee_name` | string | | |\n| `total_sale_amount_oc` | number | yes | |\n| `total_sale_amount` | number | yes | |\n| `total_discount_amount_oc` | number | | |\n| `total_discount_amount` | number | | |\n| `total_vat_amount_oc` | number | yes | |\n| `total_vat_amount` | number | yes | |\n| `total_amount_oc` | number | yes | |\n| `total_amount` | number | yes | |\n| `is_sale_with_outward` | boolean | | Auto-generate stock-out slip |\n| `detail` | object[] | yes | |\n| `detail[].inventory_item_code` | string | | Item/product code in MISA. Skip when is_description=true |\n| `detail[].inventory_item_name` | string | yes | Item/product display name |\n| `detail[].inventory_item_type` | 0 \\\\| 1 \\\\| 2 \\\\| 3 | | Item type \u2014 0 goods, 1 finished, 2 service, 3 raw material |\n| `detail[].description` | string | | Line description |\n| `detail[].is_description` | boolean | | Description-only line \u2014 when true, only `description` is required |\n| `detail[].stock_code` | string | | Warehouse code in MISA |\n| `detail[].unit_name` | string | | Unit of measure name |\n| `detail[].quantity` | number | yes | Line quantity |\n| `detail[].unit_price` | number | yes | Unit price (in currency_id of voucher) |\n| `detail[].amount_oc` | number | yes | Line total in original currency |\n| `detail[].amount` | number | yes | Line total in accounting currency (= amount_oc when VND) |\n| `detail[].discount_rate` | number | | Line discount percent |\n| `detail[].discount_amount_oc` | number | | Discount amount, original currency |\n| `detail[].discount_amount` | number | | Discount amount, accounting currency |\n| `detail[].vat_rate` | number | yes | VAT percent. 0/5/8/10 for standard, -1 exempt, -2 not creditable, -3 other (set other_vat_rate) |\n| `detail[].other_vat_rate` | number | | Custom VAT percent \u2014 required when vat_rate=-3 |\n| `detail[].vat_amount_oc` | number | yes | VAT amount, original currency |\n| `detail[].vat_amount` | number | yes | VAT amount, accounting currency |\n| `detail[].debit_account` | string | yes | Chart-of-accounts code on debit side, e.g. 131 |\n| `detail[].credit_account` | string | yes | Chart-of-accounts code on credit side, e.g. 511 |\n| `detail[].sale_account` | string | | |\n| `detail[].vat_account` | string | | |\n| `detail[].inventory_account` | string | | |\n| `detail[].sort_order` | integer | | |\n| `detail[].lot_no` | string | | |\n| `detail[].expiry_date` | string | | ISO date |\n| `detail[].organization_unit_code` | string | | Department/branch code |\n| `connected_account_id` | string | yes | MISA connected account to push to |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `queued` | boolean | yes | |\n| `org_refid` | string | yes | |\n| `error_code` | string | | |\n| `error_message` | string | | |\n\n## outlook\n\n### outlook_count_emails (workflow step)\n\n- Count Outlook emails matching a filter or search query.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `search` | string | | KQL expression |\n| `filter` | string | | OData expression |\n| `folder` | string | | Mail folder: well-known name or folder ID |\n| `user_id` | string | | Mailbox owner user ID. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `count` | number | yes | Number of matching emails |\n\n### outlook_query_emails (workflow step)\n\n- Search and filter Outlook emails.\n- Two modes: search (KQL keywords) or structured filters (from, to, subject, dates, etc.).\n- When search is provided, structured filters are ignored. ## KQL search syntax (for the search field) Bare keywords search across from, subject, and body.\n- Property restrictions (no space after colon): - People: from:, to:, cc:, bcc:, participants: (all people fields), recipients: (to+cc+bcc) - Content: subject:, body:, "exact phrase" - Attachments: hasAttachments:true, attachment:filename* - Dates: received:2024-01-01, received:2024-01-01..2024-01-31, received>=2024-01-01, received:today, received:"this week", sent:2024-06-01 - Size (bytes): size>1048576, size:1..10485760 - Status: importance:high, isRead:false, kind:email Combining (UPPERCASE required): AND, OR, NOT, - (exclude), () for grouping.\n- Prefix wildcard: subject:set* (end of word only, no *prefix or *mid*dle).\n- Examples: - from:john@contoso.com AND subject:"quarterly report" - participants:alice importance:high hasAttachments:true - body:contract AND received>=2024-01-01 AND received<=2024-06-30 - (from:legal OR from:compliance) AND subject:contract - attachment:annual* received:"this month"\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `search` | string | | KQL search query \u2014 overrides all structured filter fields when provided |\n| `from` | string | | Sender email address exact match |\n| `to` | string | | Recipient email address exact match |\n| `subject` | string | | Subject starts with this text |\n| `after` | string | | ISO date \u2014 received on or after |\n| `before` | string | | ISO date \u2014 received before |\n| `is_read` | boolean | | |\n| `is_draft` | boolean | | |\n| `has_attachments` | boolean | | |\n| `importance` | "low" \\\\| "normal" \\\\| "high" | | |\n| `is_flagged` | boolean | | Whether the email is flagged for follow-up |\n| `classification` | "focused" \\\\| "other" | | Focused Inbox classification |\n| `category` | string | | Color category name |\n| `conversation_id` | string | | Outlook conversation ID \u2014 retrieves all messages in a thread |\n| `folder` | string | | Mail folder: well-known name (inbox, sentitems, drafts, deleteditems, junkemail, archive) or folder ID |\n| `skip` | number | | Number of results to skip for pagination. |\n| `max_result` | number | yes | Maximum number of emails to retrieve (1-50) |\n| `user_id` | string | | The ID of the user whose mailbox the email belongs to. If not provided, the connected account\'s user ID (me) will be used. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `messages` | object[] | yes | List of matching Outlook messages |\n| `messages[].id` | string | yes | Outlook message ID |\n| `messages[].date` | string | yes | Email date in the workspace timezone, with its offset |\n| `messages[].from` | string | yes | Sender email address |\n| `messages[].subject` | string | yes | Email subject |\n| `messages[].to` | string | yes | Recipient email address |\n| `messages[].cc` | string \\\\| null | yes | CC recipients |\n| `messages[].bcc` | string \\\\| null | yes | BCC recipients |\n| `messages[].snippet` | string | yes | Short plain-text preview of the email body (first ~255 chars) |\n| `messages[].conversationId` | string | | Outlook conversation ID |\n| `messages[].receivedDateTime` | string | | Received date-time in ISO format |\n| `messages[].hasAttachments` | boolean | | Whether the email has attachments |\n| `messages[].importance` | string | | Email importance level |\n| `messages[].isRead` | boolean | | Whether the email has been read |\n| `messages[].isDraft` | boolean | | Whether the email is a draft |\n| `messages[].flagStatus` | string | | Flag status: notFlagged, complete, or flagged |\n| `messages[].classification` | string | | Focused Inbox classification: focused or other |\n| `messages[].categories` | string[] | | Color categories assigned to the email |\n\n### outlook_query_folders (workflow step)\n\n- List mail folders in an Outlook mailbox.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `parent_folder_id` | string | | Parent folder ID to list child folders. Omit for top-level folders. |\n| `name` | string | | Match folders by display name prefix. |\n| `include_hidden` | boolean | | Include hidden system folders |\n| `max_result` | number | | Maximum number of folders to return (1-100) |\n| `skip` | number | | Number of results to skip for pagination |\n| `user_id` | string | | The ID of the user whose mailbox to query. Defaults to the connected account owner. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `folders` | object[] | yes | List of mail folders |\n| `folders[].id` | string | yes | Folder ID |\n| `folders[].displayName` | string | yes | Folder display name |\n| `folders[].parentFolderId` | string | | Parent folder ID |\n| `folders[].childFolderCount` | number | yes | Number of child folders |\n| `folders[].totalItemCount` | number | yes | Total number of items in the folder |\n| `folders[].unreadItemCount` | number | yes | Number of unread items |\n| `folders[].isHidden` | boolean | yes | Whether the folder is hidden |\n\n### outlook_read_email (workflow step)\n\n- Fetch an Outlook email\'s body and the list of its attachments.\n- Attachments are stored as workspace files \u2014 the result names each one\'s file_id; read one with view_files, attach one to a record with update_records. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `message_id` | string | yes | The ID of the Outlook message to retrieve |\n| `user_id` | string | | The ID of the user whose mailbox the email belongs to. If not provided, the connected account\'s user ID (me) will be used. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message` | object | yes | The full Outlook message |\n| `message.id` | string | | Outlook message ID |\n| `message.date` | string | yes | Email date in the workspace timezone, with its offset |\n| `message.from` | string | yes | Sender email address |\n| `message.subject` | string | yes | Email subject |\n| `message.to` | string | yes | Recipient email address |\n| `message.cc` | string \\\\| null | yes | CC recipients |\n| `message.bcc` | string \\\\| null | yes | BCC recipients |\n| `message.inReplyTo` | string \\\\| null | yes | In-Reply-To header |\n| `message.replyTo` | string \\\\| null | yes | Reply-To header |\n| `message.references` | string[] \\\\| null | yes | References header |\n| `message.conversationId` | string | | Outlook conversation ID |\n| `message.receivedDateTime` | string | | Received date-time in ISO format |\n| `message.hasAttachments` | boolean | | Whether the email has attachments |\n| `message.importance` | string | | Email importance level |\n| `message.isRead` | boolean | | Whether the email has been read |\n| `message.body` | object | yes | Email body |\n| `message.body.content` | string | yes | Body content |\n| `message.body.contentType` | "text" \\\\| "html" | yes | Body content type |\n| `message.attachments` | object[] | | Email attachments |\n| `message.attachments[].id` | string | yes | Outlook attachment ID |\n| `message.attachments[].fileName` | string | | Attachment filename as stored, which may differ from the sender\'s |\n| `message.attachments[].contentType` | string | | MIME type as stored, recovered from the filename when Graph declared a generic one |\n| `message.attachments[].size` | number | | Stored size in bytes |\n| `message.attachments[].file_id` | string | | Stored file ID \u2014 pass to view_files to read it, or to update_records to attach it (workflow step, chat) |\n\n### outlook_reply_email (workflow step)\n\n- Draft or send a reply to an Outlook email.\n- Use reply_body (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `original_message_id` | string | yes | |\n| `reply_body` | string | | Reply body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n| `user_id` | string | | Mailbox user ID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string | yes | The original sender the reply went to |\n\n### outlook_send_email (workflow step)\n\n- Draft or send an Outlook email.\n- Use content (markdown) or template_id + template_data \u2014 not both.\n- Attach stored files by passing their file_ids.\n- Leaves an unsent draft unless draft is false, and while drafting \'to\' may be omitted to leave the recipient line for the member to fill in.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | The Lotics connected account ID (cac_xxx), not the Microsoft user ID |\n| `to` | string | | Recipient email(s), comma-separated. Required unless using a template. |\n| `subject` | string | | |\n| `cc` | string | | CC recipients, comma-separated |\n| `bcc` | string | | BCC recipients, comma-separated |\n| `content` | string | | Email body (markdown). Mutually exclusive with template_id. |\n| `template_id` | string | | |\n| `template_data` | object | | |\n| `draft` | boolean | | Leave true to save an unsent draft the member reviews. Pass false only to deliver the message now \u2014 delivery is irreversible. |\n| `file_ids` | string[] | yes | |\n| `user_id` | string | | Mailbox user ID |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | string | yes | The ID of the sent message or saved draft |\n| `subject` | string | yes | The subject as sent |\n| `html` | string | yes | The body as sent, rendered to HTML |\n| `markdown` | string | | The body as written in markdown; absent when a template rendered it |\n| `to` | string \\\\| null | yes | The recipients as sent, comma-separated; null on a draft saved without recipients |\n\n## press\n\n### press_button (workflow step)\n\n- Run a button field\'s action against a record \u2014 equivalent to a user clicking the button in the UI.\n- Buttons are a legacy field type; the action must already be wired.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `table_id` | string | yes | |\n| `record_id` | string | yes | |\n| `field_key` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `execution_id` | string | | |\n| `status` | "success" \\\\| "error" | | |\n| `message` | string | | |\n\n## rename\n\n### rename_file (workflow step)\n\n- Get a file under another name: a new file over the same bytes.\n- Write its file_id into the files field in place of the old one; every other place holding the old file keeps its name.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `filename` | string | yes | The whole new name, keeping the file\'s extension exactly. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n## save\n\n### save_file (workflow step)\n\n- Save a file from a URL to the workspace.\n- Max 20MB.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `url` | string | yes | Public URL to download the file from (https:// or http://). |\n| `filename` | string | | Filename with extension, e.g. \'report.pdf\'. Derived from URL if omitted. |\n| `mime_type` | string | | MIME type, e.g. \'application/pdf\'. Derived from URL response if omitted. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `id` | string | yes | File ID. |\n| `filename` | string | yes | |\n| `mime_type` | string | yes | |\n| `url` | string | yes | |\n\n## send\n\n### send_notification (workflow step)\n\n- Send a notification to workspace members. recipients is an array of principals: { type, id }.\n- All members: [{ type: "organization", id: "{{organization_id}}" }] Who triggered: [{ type: "member", id: "{{triggered_by_member_id}}" }] A group: [{ type: "member_group", id: "<group_id>" }]\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `recipients` | any[] | yes | List of recipients. Each entry is a principal: { type: \'member\', id } \\| { type: \'member_group\', id } \\| { type: \'organization\', id } |\n| `title` | string | yes | |\n| `body` | string | yes | |\n| `level` | "urgent" \\\\| "warning" \\\\| "info" \\\\| "success" | | Notification level: urgent, warning, info, success |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message` | string | yes | |\n| `recipients_count` | number | yes | |\n\n## telegram\n\n### telegram_list_chats (workflow step)\n\n- List the chats that messaged a Telegram bot, or added it, in the last 24 hours \u2014 where a chat id comes from.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Telegram connected account |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `chats` | object[] | yes | |\n| `chats[].chat_id` | string | yes | |\n| `chats[].type` | string | yes | private, group, supergroup or channel |\n| `chats[].name` | string | yes | |\n| `chats[].username` | string | | |\n| `chats[].last_seen_at` | string | yes | ISO timestamp of the chat\'s latest update |\n\n### telegram_send_document (workflow step)\n\n- Send a stored file from a Telegram bot to a chat, as a document with an optional caption.\n- The chat must have messaged the bot first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Telegram connected account |\n| `chat_id` | string | yes | Numeric chat id, or the @username of a public channel or group |\n| `file_id` | string | yes | |\n| `caption` | string | | |\n| `parse_mode` | "HTML" \\\\| "MarkdownV2" | | How the text is formatted: Telegram\'s HTML tags or its MarkdownV2 syntax |\n| `disable_notification` | boolean | | Deliver without a notification sound |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | number | yes | |\n| `chat_id` | string | yes | |\n| `sent_at` | string | yes | ISO timestamp |\n\n### telegram_send_message (workflow step)\n\n- Send a text message from a Telegram bot to a chat.\n- The chat must have messaged the bot first.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | Telegram connected account |\n| `chat_id` | string | yes | Numeric chat id, or the @username of a public channel or group |\n| `text` | string | yes | |\n| `parse_mode` | "HTML" \\\\| "MarkdownV2" | | How the text is formatted: Telegram\'s HTML tags or its MarkdownV2 syntax |\n| `disable_notification` | boolean | | Deliver without a notification sound |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `message_id` | number | yes | |\n| `chat_id` | string | yes | |\n| `sent_at` | string | yes | ISO timestamp |\n\n## threads\n\n### threads_create_post (workflow step)\n\n- Publish a post to the connected Threads account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `text` | string | yes | |\n| `link` | string | | Shown as a link card. A post carries either a link or media. |\n| `media_file_ids` | string[] | | One image or video, or 2\u201320 images and videos as a carousel. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## vietcombank\n\n### vietcombank_convert_currency (workflow step)\n\n- Convert between currencies using live Vietcombank exchange rates.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `from_currency` | string | yes | Source currency code (e.g., \'USD\', \'EUR\', \'JPY\') |\n| `to_currency` | string | yes | Target currency code (e.g., \'USD\', \'EUR\', \'JPY\') |\n| `amount` | number | yes | The amount to convert (must be positive) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `converted_amount` | number | yes | The converted amount |\n| `exchange_rate` | number | yes | The exchange rate |\n\n## view\n\n### view_files (workflow step)\n\n- View one or more files in a single call.\n- Two modes: - file_ids: explicit list of file IDs (use for cherry-picked files). - source: every file attached to a record\'s file field (use when you want all photos/attachments on a record).\n- Provide exactly one.\n- Reads EVERY readable format: images, PDFs, Word (.docx and legacy .doc), Excel (.xlsx and legacy .xls), CSV, and text formats (.txt, .md, .json, .xml, .yaml, .html).\n- Batch limit: 100 files.\n- Use this whenever you are handed a file_id you cannot already see \u2014 images and PDFs arrive in your context natively, but every other format is a reference you must open with this tool.\n- Files already in your context \u2014 those the user attached to the current message, and those an earlier view_files call returned this conversation \u2014 return a text-only "already in context" reference instead of re-emitting bytes. (workflow step, chat)\n- You do not need to re-call view_files to look at the same image again; scroll back to the original tool_result. (workflow step, chat)\n- Calling with already-seen file_ids is safe but wasteful.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_ids` | string[] | | |\n| `source` | object | | |\n| `source.record_id` | string | yes | |\n| `source.field_key` | string | yes | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `files` | object[] | yes | |\n| `files[].file_id` | string | yes | |\n| `files[].filename` | string | yes | |\n| `files[].mime_type` | string | yes | |\n| `files[].content` | object | | |\n| `files[].already_in_context` | boolean | | |\n| `files[].error` | string | | |\n\n## wininvoice\n\n### wininvoice_create_or_update_invoice (workflow step)\n\n- Create or update a Vietnamese e-invoice in WinInvoice. invRef identifies the invoice: a value that already has one resolves to that invoice rather than issuing a new one, and isExisting: 1 means it came back unchanged with nothing issued.\n- Derive invRef from what is being billed rather than from a record that may later describe something else.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `invName` | string | yes | Invoice sample name, ex: 1,2,6 |\n| `invSerial` | string | yes | Invoice sign, ex: C24TAA, K24TXX |\n| `invNumber` | string | yes | Invoice number |\n| `invDate` | string | yes | Invoice date (format: yyyy/mm/dd) |\n| `invCustomer` | 0 \\\\| 1 | | Customer is not company? Value 1\\|0 |\n| `invRef` | string | yes | 3rd party Bill-ID. Identifies the invoice: a value that already has an invoice resolves to that invoice instead of issuing a new one |\n| `invRefDate` | string | | Bill date (format: yyyy/mm/dd) |\n| `billNumber` | string | | Bill number - extend |\n| `buyerTax` | string | | Buyer tax-number |\n| `buyerCode` | string | | Buyer-id |\n| `buyerName` | string | yes | Buyer full name |\n| `buyerCompany` | string | | Buyer company/ organization name |\n| `buyerAddress` | string | | Buyer full address |\n| `buyerAcc` | string | | Buyer bank account number |\n| `buyerBank` | string | | Buyer bank name |\n| `buyerEmail` | string | | Buyer email |\n| `buyerPhone` | string | | Buyer phone number |\n| `buyerFax` | string | | Buyer fax number |\n| `buyerCitizenIDNumber` | string | | Citizen Identification Number |\n| `buyerPassportNumber` | string | | Passport Number |\n| `govUnitCode` | string | | Budget-related Unit Code |\n| `invSubTotal` | number | yes | Invoice Subtotal (not include VAT, not include discount) |\n| `invVatRate` | number | yes | VAT Rate: 0,5,8,10 (0%,5%,8%,10%), -1 (Not taxable), -2 (Not declare and pay taxes) |\n| `invVatAmount` | number | yes | VAT Amount |\n| `invTotalAmount` | number | yes | Invoice Total |\n| `invPayment` | string | | Payment method |\n| `invExchangeRate` | number | | Exchange rate (to VietNam \u0111\u1ED3ng) |\n| `invCurrency` | string | | Payment currency |\n| `note` | string | | Note for discount |\n| `invAutoSign` | 0 \\\\| 1 | | Request sign invoice immediately after create successful? Value 1\\|0 |\n| `privateCode` | string | | The UID string (10-12 chars), the buyer will use this code to lookup invoice (after invoice signed). If blank, WinInvoice will generate automatically |\n| `option` | 0 \\\\| 1 \\\\| 2 \\\\| 3 \\\\| 4 | | Invoice type: 0 (Normal invoice), 1 (Information-adjustment invoice), 2 (Increases-adjustment invoice), 3 (Decreases-adjustment invoice), 4 (Replacement invoice - original will be marked as deleted when signed) |\n| `invCodeOld` | string | | Original invoice number (required for adjustment/replacement invoices) |\n| `invNameOld` | string | | Original invoice sample name (required for adjustment/replacement invoices) |\n| `invSignOld` | string | | Original invoice sign (required for adjustment/replacement invoices) |\n| `create04SSHDDT` | 0 \\\\| 1 | | Auto create 04/SS-H\u0110\u0110T? Value 1\\|0 (default 1) |\n| `reason04SSHDDT` | string | | 04/SS-H\u0110\u0110T Reason |\n| `isDscnForSaleInv` | 0 \\\\| 1 | | Apply an 8% tax reduction to the sales invoice? Value 1\\|0 (default 0) |\n| `saleInvVATRate` | number | | Tax rate based on revenue (Only applicable when isDscnForSaleInv = 1) |\n| `saleInvDscnAmnt` | number | | The reduction amount determined based on revenue and corresponding tax rate (Only applicable when isDscnForSaleInv = 1) |\n| `privateNote` | string | | Used for internal notes |\n| `items` | object[] | yes | Array of item/product in this bill |\n| `items[].itemNo` | string | | Item number |\n| `items[].itemCode` | string | | Product/Item code |\n| `items[].itemName` | string | yes | Name of product |\n| `items[].itemPromo` | 0 \\\\| 1 | | Is promotional product (gift)? Value 1\\|0 (default 0) |\n| `items[].isDscnItem` | 0 \\\\| 1 | | Is the discount line? Value 1\\|0 (default 0) |\n| `items[].itemUnit` | string | | Product Unit |\n| `items[].itemQuantity` | number | yes | Product quantity |\n| `items[].itemPrice` | number | yes | Product unit price (not include VAT) |\n| `items[].itemVatRate` | number | | Product VAT rate |\n| `items[].itemVatAmnt` | number | | Product VAT Amount |\n| `items[].itemDscnAmnt` | number | | Product Discount Amount |\n| `items[].itemAmountNoVat` | number | | Product Amount (not include VAT)(not include item discount) |\n| `items[].adjustType` | "PRICE" \\\\| "QTTY" | | Type of adjustment: PRICE (Adjust unit price of item in original invoice), QTTY (Adjust quantity of item in original invoice). This field has effect when value of option field is 2 or 3 |\n| `items[].itemPack` | string | | Product Lot |\n| `items[].itemDate` | string | | Product expiration date |\n| `items[].itemNote` | string | | Product note |\n| `items[].specialGoodsType` | 0 \\\\| 1 \\\\| 2 \\\\| 3 | | Special category of goods: 0 (Normal item/good), 1 (Cars or motorcycles), 2 (Transportation service), 3 (Transportation services on digital and e-commerce platforms) |\n| `items[].specialGoodsInfo` | object | | Data requirements for specialized goods based on specialGoodsType |\n| `items[].specialGoodsInfo.chassisNumb` | string | | Chassis Number (for cars/motorcycles - specialGoodsType = 1) |\n| `items[].specialGoodsInfo.engineNumb` | string | | Engine Number (for cars/motorcycles - specialGoodsType = 1) |\n| `items[].specialGoodsInfo.licensePlate` | string | | License plate of the transport vehicle (for transportation service - specialGoodsType = 2) |\n| `items[].specialGoodsInfo.senderName` | string | | Sender\'s Name (for shipping service - specialGoodsType = 3) |\n| `items[].specialGoodsInfo.senderAddress` | string | | Sender\'s Address (for shipping service - specialGoodsType = 3) |\n| `items[].specialGoodsInfo.senderTaxCode` | string | | Sender\'s Tax Code (for shipping service - specialGoodsType = 3) |\n| `items[].specialGoodsInfo.senderCitizenIDNumber` | string | | Sender\'s Citizen Identification Number (for shipping service - specialGoodsType = 3) |\n| `items[].isNoteItem` | 0 \\\\| 1 | | Is the note line? Value 1\\|0 (default 0) |\n| `connected_account_id` | string | yes | The ID of the connected account |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `oid` | string | yes | WinInvoice\'s UID |\n| `invCode` | string | yes | Invoice number (is \'0000000\' if has not signed) |\n| `invRef` | string | yes | 3rd party Bill-ID |\n| `invSign` | string | yes | Invoice sign, (as invSerial) |\n| `invDate` | string | yes | Invoice date, format yyyy-mm-dd |\n| `invName` | string | yes | Full invoice sample (invName + invSign) |\n| `privateCode` | string | yes | Private lookup code for buyer |\n| `itemTotal` | number | | Count of products in invoice |\n| `itemError` | number | | Count of error-products in invoice |\n| `govTranfer` | number | yes | Invoice has transferred to GOV? 1\\|0 |\n| `govTranID` | string | yes | Transaction ID used to transfer invoice to GOV |\n| `govTranferErr` | number | yes | ansferred to GOV has FAIL? 1\\|0 |\n| `govTranText` | string | yes | Error message if transfer to GOV has FAIL |\n| `govCode` | string | yes | The UID string issue by GOV for this invoice |\n| `autoSign` | number | | Has request sign immediately ? 1\\|0 |\n| `isExisting` | 0 \\\\| 1 | | 1 when invRef already had an invoice and this call returned it unchanged \u2014 no invoice was issued |\n| `link` | string | yes | Link to view the invoice |\n\n## word\n\n### word_create_document (workflow step)\n\n- Create a new Word document (.docx), optionally with initial paragraphs and headings.\n- Use {{variable_name}} syntax in text to create template placeholders for data injection via create_word_template. (workflow step, chat, MCP, CLI)\n- Input: { filename, content?: [{ type: "paragraph"\\|"heading", text, level?: 1-6 }] } filename is without the .docx extension (added automatically).\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `filename` | string | yes | Name of the file (without .docx extension) |\n| `content` | object[] | | |\n| `content[].type` | "paragraph" \\\\| "heading" | yes | |\n| `content[].text` | string | yes | |\n| `content[].level` | number | | Heading level (1-6), required when type is heading |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n\n### word_find_text (workflow step)\n\n- Search for text across all paragraphs and table cells in a Word document.\n- Input: { file_id, query: "search text", match_type?: "contains"\\|"exact"\\|"regex", max_results?: number } Default match_type is "contains".\n- Returns matching element indices, types, and text content.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `query` | string | yes | |\n| `match_type` | "contains" \\\\| "exact" \\\\| "regex" | | |\n| `max_results` | number | | |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `matches` | object[] | yes | |\n| `matches[].index` | number | yes | |\n| `matches[].type` | "paragraph" \\\\| "table" \\\\| "other" | yes | |\n| `matches[].text` | string | yes | |\n| `total` | number | yes | |\n\n### word_get_content (workflow step)\n\n- Read a Word document\'s full native content, paginated.\n- Every element returned is COMPLETE \u2014 full paragraph text and full table cells \u2014 unlike view_files, whose quick-look preview truncates long paragraphs and tables. (workflow step, chat)\n- Input: { file_id, offset?, limit? }.\n- Returns elements with their 0-based indices plus next_offset when more remains; call again with offset=next_offset to continue.\n- A page may end before `limit` to keep elements whole \u2014 follow next_offset, don\'t infer completion from the count.\n- A selection mark \u2014 a highlighted, shaded or coloured run, or a shaded table cell \u2014 surfaces in `marked_text` (the paragraph with each mark shown where it falls, which is what tells you WHICH of two identical `\u25A1` was picked), `highlighted_spans` (the marked text alone) and `marked_cells` (per table), so a form\'s chosen option reads correctly even when it is marked by colour rather than a tick.\n- Bold is OFF by default and reported only with `include_bold`, as a FACT rather than a selection: nearly every heading and label is bold, so on most documents it is noise.\n- Reach for it when a form\'s chosen option carries no glyph and no colour \u2014 some forms record the choice by bolding the chosen option\'s label, and then bold is the tick\'s only trace.\n- On a PARAGRAPH, `formatting_hint` already says the line contains bold (it cannot say which run \u2014 that is what `bold_spans` adds).\n- A TABLE carries no such hint, so on a criteria or option table pass `include_bold` rather than waiting for a signal.\n- Read the result against its siblings: a wholly bold heading or header row is structure, one bold label among plain ones is the choice.\n- Footnotes surface in `footnote_refs` (per paragraph) and `footnote_cells` (per table) with the note\'s own text, since a clause often carries its rule in the footnote rather than the sentence.\n- `orphaned_footnotes` lists notes the file carries with no anchor left in the body \u2014 those render nowhere.\n- Pictures surface in `images` on the element holding them, so a paragraph carrying only a picture is not mistaken for an empty one.\n- `headers_footers` carries header and footer text, which sits outside the body and so has no element index \u2014 a form often states which form it is there rather than in its text.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `offset` | integer | | Element index to start from (pass the prior call\'s next_offset) |\n| `limit` | integer | | Max elements to return |\n| `include_bold` | boolean | | Also report WHERE the bold runs are (bold_spans / bold_cells). Off unless asked for \u2014 nearly every heading and label is bold, so it is noise on most documents and the answer on a form that ticks by bolding. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `elements` | object[] | yes | |\n| `elements[].index` | number | yes | |\n| `elements[].type` | "paragraph" \\\\| "table" \\\\| "other" | yes | |\n| `elements[].text` | string | | |\n| `elements[].rows` | number | | |\n| `elements[].cols` | number | | |\n| `elements[].cells` | string[][] | | |\n| `elements[].heading_level` | number | | |\n| `elements[].list_type` | "bullet" \\\\| "numbered" | | |\n| `elements[].list_level` | number | | |\n| `elements[].formatting_hint` | string | | |\n| `elements[].highlighted_spans` | string[] | | |\n| `elements[].bold_spans` | string[] | | |\n| `elements[].marked_text` | string | | |\n| `elements[].images` | object[] | | |\n| `elements[].images[].name` | string | | |\n| `elements[].images[].width_cm` | number | | |\n| `elements[].images[].height_cm` | number | | |\n| `elements[].marked_cells` | object[] | | |\n| `elements[].marked_cells[].row` | number | yes | |\n| `elements[].marked_cells[].col` | number | yes | |\n| `elements[].marked_cells[].spans` | string[] | yes | |\n| `elements[].bold_cells` | object[] | | |\n| `elements[].bold_cells[].row` | number | yes | |\n| `elements[].bold_cells[].col` | number | yes | |\n| `elements[].bold_cells[].spans` | string[] | yes | |\n| `elements[].footnote_refs` | object[] | | |\n| `elements[].footnote_refs[].mark` | number | yes | The superscript number the reader sees |\n| `elements[].footnote_refs[].id` | string | yes | |\n| `elements[].footnote_refs[].offset` | number | yes | Character offset into this element\'s text where the mark sits |\n| `elements[].footnote_refs[].text` | string | | |\n| `elements[].footnote_cells` | object[] | | |\n| `elements[].footnote_cells[].row` | number | yes | |\n| `elements[].footnote_cells[].col` | number | yes | |\n| `elements[].footnote_cells[].refs` | object[] | yes | |\n| `elements[].footnote_cells[].refs[].mark` | number | yes | The superscript number the reader sees |\n| `elements[].footnote_cells[].refs[].id` | string | yes | |\n| `elements[].footnote_cells[].refs[].offset` | number | yes | Character offset into this element\'s text where the mark sits |\n| `elements[].footnote_cells[].refs[].text` | string | | |\n| `total` | number | yes | |\n| `next_offset` | number \\\\| null | yes | |\n| `orphaned_footnotes` | object[] | | Notes the file still carries with no anchor left in the body \u2014 they render nowhere |\n| `orphaned_footnotes[].id` | string | yes | |\n| `orphaned_footnotes[].text` | string | yes | |\n| `headers_footers` | object[] | | Header and footer text \u2014 outside the body, so it carries no element index |\n| `headers_footers[].kind` | "header" \\\\| "footer" | yes | |\n| `headers_footers[].text` | string | yes | |\n\n### word_get_table_data (workflow step)\n\n- Read the full contents of a table in a Word document as a 2D string array.\n- Input: { file_id, table_index: number, max_rows? } table_index is the 0-based element index from view_files (must point to a table element). (workflow step, chat)\n- Returns data[][] with row_count and col_count.\n- A cell carrying a selection mark \u2014 a highlighted or shaded run, or a shaded cell \u2014 is listed in `marked_cells` ({row, col} aligned with data, spans = the marked text; empty spans = a shaded blank), so a criteria row\'s chosen option reads correctly even when it is marked by colour rather than a tick.\n- `bold_cells` lists the bold cells the same way, but ONLY with `include_bold` \u2014 header rows and label columns are bold, so it is noise on most tables.\n- Nothing in the default output hints that a table contains bold, so pass it whenever a criteria or option table shows no glyph and no colour: some forms record the choice by bolding that row\'s label and it is the tick\'s only trace.\n- It is a fact, never a selection \u2014 a whole bold header row is structure, one bold label among plain siblings is the choice.\n- Read-only \u2014 does not modify the file.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `table_index` | number | yes | 0-based element index of the table |\n| `max_rows` | number | | Max rows to return; all rows when omitted |\n| `include_bold` | boolean | | Also report which cells are bold (bold_cells). Off unless asked for \u2014 header rows and label columns are bold, so it is noise on most tables and the answer on one that ticks by bolding. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `data` | string[][] | yes | |\n| `row_count` | number | yes | |\n| `col_count` | number | yes | |\n| `marked_cells` | object[] | | |\n| `marked_cells[].row` | number | yes | |\n| `marked_cells[].col` | number | yes | |\n| `marked_cells[].spans` | string[] | yes | |\n| `bold_cells` | object[] | | |\n| `bold_cells[].row` | number | yes | |\n| `bold_cells[].col` | number | yes | |\n| `bold_cells[].spans` | string[] | yes | |\n\n### word_insert_conditional (workflow step)\n\n- Insert {{IF variable_name}}...{{END-IF variable_name}} command rows or paragraphs into a Word document to define a conditional section.\n- For table_rows: wraps the specified row range with command rows.\n- Requires table_index, start_row, end_row.\n- For paragraphs: wraps the specified element range with command paragraphs.\n- Requires start_index, end_index.\n- Use view_files to find element indices first. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `variable_name` | string | yes | Boolean variable name |\n| `region_type` | "table_rows" \\\\| "paragraphs" | yes | |\n| `table_index` | integer | | Index of the table element (required for table_rows) |\n| `start_row` | integer | | First row to wrap (required for table_rows) |\n| `end_row` | integer | | Last row to wrap, inclusive (required for table_rows) |\n| `start_index` | integer | | First element index to wrap (required for paragraphs) |\n| `end_index` | integer | | Last element index to wrap, inclusive (required for paragraphs) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n\n### word_insert_loop (workflow step)\n\n- Insert {{FOR loop_var IN variable_name}}...{{END-FOR loop_var}} command rows or paragraphs into a Word document to define a repeating section.\n- Inside the loop, reference each item\'s fields with a $ prefix: {{$loop_var.field}} (or {{$loop_var}} for a scalar item, {{$idx}} for the 0-based index).\n- Without the $ prefix, generation fails.\n- For table_rows: wraps the specified row range with command rows.\n- Requires table_index, start_row, end_row.\n- For paragraphs: wraps the specified element range with command paragraphs.\n- Requires start_index, end_index.\n- Use view_files to find element indices first. (workflow step, chat)\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `variable_name` | string | yes | Array variable name |\n| `loop_variable` | string | yes | Iterator name used inside the loop, e.g. item |\n| `region_type` | "table_rows" \\\\| "paragraphs" | yes | |\n| `table_index` | integer | | Index of the table element (required for table_rows) |\n| `start_row` | integer | | First row to wrap (required for table_rows) |\n| `end_row` | integer | | Last row to wrap, inclusive (required for table_rows) |\n| `start_index` | integer | | First element index to wrap (required for paragraphs) |\n| `end_index` | integer | | Last element index to wrap, inclusive (required for paragraphs) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n\n### word_replace_text (workflow step)\n\n- Find and replace text across all paragraphs and table cells in a Word document.\n- Replaces all occurrences unless max_replacements is set.\n- A match spanning several runs collapses them into the first run\'s formatting, so tick a checkbox by matching the box character on its own \u2014 \u2610 (U+2610) becomes \u2612 (U+2612), keeping the font that draws both.\n- Matching the phrase around the box drags it into the body font; replacing it with a letter leaves a bare letter beside the boxes still showing \u2610.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the Word (.docx) file |\n| `find` | string | yes | |\n| `replace` | string | yes | |\n| `match_type` | "contains" \\\\| "exact" \\\\| "regex" | | `exact` matches a whole paragraph. None of the three is case-sensitive. |\n| `max_replacements` | number | | Max number of replacements (default: unlimited) |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | |\n| `replacements_made` | number | yes | |\n\n## x\n\n### x_create_post (workflow step)\n\n- Publish a post to the connected X account.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `connected_account_id` | string | yes | |\n| `text` | string | yes | |\n| `media_file_ids` | string[] | | Up to 4 images, or one GIF, or one video. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `post_id` | string | yes | |\n| `url` | string | yes | |\n\n## zip\n\n### zip_files (workflow step)\n\n- Bundle multiple files into a zip archive.\n- Max 100 files.\n- An item in file_ids is a file id \u2014 or `{ id, name }` to name that entry in the archive: a file name, never a path, carrying the stored extension.\n- A bare id keeps the stored filename.\n\n**Inputs**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_ids` | string \\\\| object[] | yes | |\n| `filename` | string | | Name for the zip file, without the .zip extension. |\n\n**Answer**\n\n| Name | Type | Required | What it is |\n| --- | --- | --- | --- |\n| `file_id` | string | yes | The ID of the generated file. |\n| `url` | string | yes | The download URL of the generated file. |\n| `filename` | string | yes | The filename of the generated file, including extension. |\n| `mime_type` | string | yes | The MIME type of the generated file. |\n';
|
|
44401
44562
|
|
|
44402
44563
|
// docs/app_bindings.md
|
|
44403
|
-
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";
|
|
44564
|
+
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, `{ bucket: { source, granularity, output } }`, or `{ band: { source, tiers, output } }`), `aggregates` |\n| `window` | `partition_by`, `order_by`, `frame?`, `aggregates?`, `functions?` |\n| `sort` | `by: [{ field_key, order }]` |\n| `limit` | `n`, `offset?` or `keyset?` |\n| `unpivot` | `passthrough`, `row_columns`, `rows` \u2014 one source row into several |\n| `unnest` | `source`, `output`, `display_output?`, `keep_empty?` \u2014 one row per element of a multi-value cell |\n\nFilters (`from_table.filter`, `filter.predicate`, an aggregate's `filter`) are the filter grammar \u2014 the\n`filters` reference \u2014 over the input's columns, taking `{{params.x}}` as values. `search` matches\nevery text-bearing field of the row, accent- and case-insensitive.\n\nA `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? }`. On an app built from a model, the alias is one the model names \u2014 an act's own\n`workflow`, or an entry of the app's `workflows`, which states `inputs`, `outputs` and `description`:\na call stating any of them for it is refused, and outputs the entry leaves out are derived from the\nbody. An alias the model names nowhere is archived by the next apply. An entry of `workflows` runs\nonly for someone who holds the app: on an app shared by a public link, a visitor's call of it is\nrefused as an alias the app does not have.\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";
|
|
44404
44565
|
|
|
44405
44566
|
// docs/document_templates.md
|
|
44406
44567
|
var document_templates_default = '# Document Templates\n\nTurn workspace data into finished documents \u2014 invoices, contracts, reports, letters,\nlabels, emails \u2014 by filling a reusable template instead of writing a file from scratch.\nYou register a template once, then generate a filled file (or a rendered email) from it as\nmany times as you like, feeding it data from records, a workflow, or an agent run.\n\nThis is a **capability + usage** guide. For the exact input schema of any tool named here,\nrun `lotics tools <tool_name>` \u2014 that is always the source of truth for arguments.\n\n## The five template types\n\n| Type | What it fills | Output | Create with |\n|---|---|---|---|\n| `pdf-form` | fields overlaid on an **uploaded PDF** at fixed positions | PDF | `create_pdf_template` (mode `form`), on the CLI or in chat |\n| `html` (a PDF) | an **HTML + Handlebars** layout you write from scratch | PDF | `create_pdf_template` (mode `html`), on the CLI or in chat |\n| `excel` | an **uploaded `.xlsx`** with `{{marker}}` cells | `.xlsx` | `create_excel_template` |\n| `word` | an **uploaded `.docx`** with `{{marker}}`s | `.docx` | `create_word_template` |\n| `email` | an **inline HTML + Handlebars** body, rendered when the email is sent | email | `create_email_template`, on the CLI or in chat |\n\nTwo shapes underneath: **file-backed** (`pdf-form`, `excel`, `word`) clone an uploaded\noffice file and mark it up; **inline** (`html`, `email`) store the markup you author directly.\n\n## The lifecycle: create \u2192 generate \u2192 chain\n\n1. **Create** a template \u2014 register the file (or inline markup) and declare a `variables`\n map (the named slots the template fills). This returns a template id (`dtl_\u2026`).\n2. **Generate** a filled file \u2014 call `generate_document` with the\n template id, a `filename` (no extension), and a `data` map keyed by your variable names.\n It substitutes the markers and returns a **generated file** (with a `file_id`).\n3. **Chain** the result \u2014 generation only *produces* the file. Attaching it to a record,\n sending it, or saving it locally is a separate step: inside a workflow, pass the returned\n `file_id` to the next step; from the CLI, `lotics download <file_id>` fetches it.\n\nEmail is the exception: `generate_document` refuses an email template. An email template is\nrendered to a subject + body **at send time** from the template and the data the send step\nsupplies \u2014 so you author it with `create_email_template` (on the CLI or in chat) and a workflow/app send step (or the\nweb composer) does the rendering and delivery. External agents send their own email directly.\n\n## Creating each type\n\n### PDF from HTML\n\nWrite a full HTML document with inline CSS and `{{variable}}` placeholders, as `create_pdf_template`\nwith mode `html` on the CLI or in chat; it renders to PDF via a headless browser. Good for invoices, reports, letters, certificates \u2014 anything whose\nlayout you control. Declare `variables` (plain text, rich `html`, an auto-rendered `table`,\nor a `list`), and optionally `format` (page size) and `orientation`. See\n`lotics tools create_pdf_template` for the exact variable shapes.\n\n### PDF form\n\nFor an existing PDF you must fill in place (a government form, a printed contract), on the CLI or in chat:\n\n1. `lotics upload ./form.pdf` \u2192 a `file_id`.\n2. `create_pdf_template` with mode `form` and that `file_id`.\n3. `analyze_pdf_template` \u2014 extracts the labels and their positions off the page.\n4. `update_pdf_template` \u2014 define the positioned fields (`text` / `checkbox` / `image`, each\n with a page + x/y/size) in `variables`.\n\nForm-mode data is scalar-only (text, numbers, checkboxes) \u2014 one value per positioned field.\n\n### Excel\n\n1. Build a `.xlsx` in Excel (or with any spreadsheet library) and put `{{marker}}`s in the cells that\n should be filled. `lotics upload ./template.xlsx` \u2192 a `file_id`.\n2. `create_excel_template` with that `template_file_id` and a `variables` map. Markers are\n **validated at create time** \u2014 a structural error (mismatched loop, unknown helper, bad\n placement) blocks the save, and declared variables with no matching marker come back in\n `unmarked_variables` (skipped, not filled).\n\nOn the CLI, `validate_excel_template` checks an uploaded file\'s markers without creating a template. Pass\nraw numbers / ISO dates / booleans at generate time \u2014 the cell\'s number format handles display.\n\n### Word\n\n1. Build a `.docx` with `{{marker}}`s where values go. `lotics upload ./template.docx`.\n2. `create_word_template` with the `template_file_id`. `variables` is optional \u2014 every\n marker in the document is derived automatically (`{{name}}` \u2192 string,\n `{{FOR \u2026 IN name}}` \u2192 array, `{{IF name}}` \u2192 boolean). When you do declare, an unmarked\n declaration comes back in `unmarked_variables` and is skipped, and markers you didn\'t\n declare are still auto-added (`derived_variables`) \u2014 the stored contract always covers\n every marker in the document.\n\nAt generate time, `data` must provide a key for **every** marker \u2014 pass `""` for fields\nthat should render blank; a missing key fails with the full list of missing markers.\n\n### Email\n\nInline HTML + Handlebars, no uploaded file \u2014 `create_email_template`, on the CLI or in chat. Declare `variables`, and optionally a default\n`subject` (which itself supports `{{variable}}` expressions) and default to/cc/bcc. The body\nand subject render from the data at send time.\n\n## Markers, at a usage level\n\nEvery type supports three shapes of substitution. The exact syntax differs per engine \u2014 read\nthe create-tool description for the reference; here is the capability:\n\n- **Scalar** \u2014 a single value in one spot: `{{name}}`, `{{total}}`. Whole-cell scalars in\n Excel preserve their type via the cell\'s number format.\n- **Repeating rows / line items** \u2014 one template row rendered once per item in a list, for\n invoice lines, order rows, tables. Excel and the HTML/email engines use a Handlebars-style\n `{{#each items}}\u2026{{/each}}`; Word uses `{{FOR item IN items}}\u2026{{$item.field}}\u2026{{END-FOR item}}`. The HTML and email types\n also offer an auto-rendered `table` variable \u2014 declare its columns and pass an array, no\n hand-written loop. (Excel always uses the `{{#each}}` marker rows \u2014 it has no auto-rendered\n `table` type; its variable types are string/number/date/boolean/array.)\n- **Conditional sections** \u2014 a block shown only when a condition holds (a "paid" stamp, an\n optional notes block). Excel/HTML/email use `{{#if}}\u2026{{else}}\u2026{{/if}}`; Word uses `{{IF name}}\u2026{{END-IF name}}`.\n\nThe `html` and `email` types format a raw value with a helper, in the template\'s `locale` (`vi` or `en`, set at\ncreate or update; none prints in the organization\'s language); an empty value prints `""`:\n`{{money total "VND"}}` \u2192 `28.000.000 \u20AB` (`en`: `\u20AB28,000,000`), `{{number qty}}` \u2192 grouped, up to 2 decimals,\n`{{date issued_on}}` \u2192 `14/09/2026` (a moment on the workspace\'s timezone; a date-fns pattern as the second argument,\nliteral text quoted: `"\'Ng\xE0y\' dd"`), and `{{words total "USD"}}` \u2192 `M\u1ED9t ngh\xECn \u0111\xF4 la M\u1EF9 n\u0103m m\u01B0\u01A1i xu` (`en`: `One\nthousand US dollars and fifty cents`; no code reads VND). A value the helper cannot read fails the render.\n\nRun `lotics tools create_excel_template`, `create_word_template`, `create_pdf_template`, or\n`create_email_template` for each engine\'s exact marker grammar and helper list \u2014 don\'t guess it.\n\n## Generating a filled file\n\n```bash\n# See what a template expects, then fill it\nlotics tools generate_document\nlotics run get_template \'{"template_id":"dtl_..."}\' # its declared variables\nlotics run generate_document \'{"document_template_id":"dtl_...","filename":"invoice-1042","data":{"company_name":"Acme","total":1042,"items":[...]}}\'\nlotics download <file_id> -o ./out/\n```\n\n- `data` is a map keyed by your variable names. Scalars for scalar/form fields; arrays for\n `table`/`list`/loop variables. `filename` excludes the extension (the type sets it).\n- `generate_document` renders the template\'s own type and returns a generated file object; take\n its `file_id` onward.\n- In a workflow, a **generate step** calls the same tool and hands the `file_id` to the next\n step (attach to a record, send as an attachment, etc.).\n\n## Discovering and managing templates\n\nFour unified tools work across all five types:\n\n| Tool | Does |\n|---|---|\n| `query_templates` | list templates (optionally filter by type: excel/word/pdf/email) |\n| `get_template` | one template\'s full definition, incl. the `variables` it expects |\n| `clone_template` | copy a template to iterate on, on the CLI or in chat |\n| `delete_template` | remove a template, on the CLI or in chat |\n\nCall `get_template` before generating when you don\'t already know a template\'s variable names.\n\n## Reaching the tools\n\n```bash\nlotics tools # all categories\nlotics tools create_word_template # one tool: full description + input schema\nlotics run <tool> \'<json-args>\' # execute (inline JSON, @file.json, or `-` for piped stdin)\n```\n\nThe template tools live in the categories **PDF Templates**, **Excel Templates**,\n**Word Templates**, **Email Templates**, and **Templates** (the unified query/get/clone/delete).\nUploading the source file for a file-backed template is `lotics upload <file>`; fetching a\ngenerated file is `lotics download <file_id>`.\n';
|