@xanots/sdk 0.0.13 → 0.0.14

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/{agent-file-refresh-GQWAAOBV.js → agent-file-refresh-2LZ2YRFZ.js} +3 -3
  4. package/dist/bin.js +2 -2
  5. package/dist/{chunk-2AY3PKF4.js → chunk-CII2TOE4.js} +2 -2
  6. package/dist/{chunk-2VTJSI6X.js → chunk-F3MRG7NI.js} +2 -2
  7. package/dist/{chunk-RLI6XD4O.js → chunk-LISUHLWI.js} +9 -9
  8. package/dist/{chunk-S3DOJOW4.js → chunk-ORZINVGQ.js} +4 -4
  9. package/dist/{chunk-SS2V2QOG.js → chunk-UQMMFW32.js} +4 -4
  10. package/dist/{chunk-OONT4ZL4.js → chunk-UXPDQFG2.js} +2 -2
  11. package/dist/{chunk-VK26K7AY.js → chunk-XBIFQWZM.js} +2 -2
  12. package/dist/cli.js +1 -1
  13. package/dist/{codegen-command-ZDHMGFWZ.js → codegen-command-5A6FVKUA.js} +5 -5
  14. package/dist/{deploy-command-CGVKRWUE.js → deploy-command-VBB7JYXH.js} +3 -3
  15. package/dist/{init-command-DNDONP3O.js → init-command-SIJUH2DD.js} +4 -4
  16. package/dist/internal.js +357 -259
  17. package/dist/{lock-commands-6U7UIJGR.js → lock-commands-4TX3BONI.js} +2 -2
  18. package/dist/node.js +1 -1
  19. package/dist/{preflight-command-UYE7SUQV.js → preflight-command-MTYGPNVT.js} +3 -3
  20. package/dist/{release-command-CMMYT6XK.js → release-command-NXCPQDFF.js} +5 -5
  21. package/dist/{upgrade-command-A75DOIUH.js → upgrade-command-GSH3M3N6.js} +5 -5
  22. package/dist/{workspace-command-YELP47SJ.js → workspace-command-XUOPLVQ4.js} +5 -5
  23. package/guides/README.md +1 -1
  24. package/guides/coverage.md +3 -2
  25. package/llms/client.md +44 -0
  26. package/llms/errors.md +33 -0
  27. package/llms/fields.md +26 -1
  28. package/llms/filters.md +4 -8
  29. package/llms/kinds-agent-mcp.md +1 -0
  30. package/llms/kinds-core.md +16 -2
  31. package/llms/kinds-knowledge.md +1 -1
  32. package/llms/kinds-realtime.md +1 -1
  33. package/llms/lambda.md +1 -1
  34. package/llms/legacy.md +1 -1
  35. package/llms/object-kinds.md +2 -2
  36. package/llms/statements-calls.md +25 -4
  37. package/llms/statements-catalog.md +1 -1
  38. package/llms/statements-runtime.md +20 -5
  39. package/llms/tests.md +2 -2
  40. package/llms/triggers.md +14 -2
  41. package/llms/values.md +2 -2
  42. package/llms-full.txt +359 -267
  43. package/llms.txt +163 -234
  44. package/manifest.json +4 -4
  45. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadDefault
3
- } from "./chunk-RLI6XD4O.js";
3
+ } from "./chunk-LISUHLWI.js";
4
4
  import "./chunk-7WRJPKGK.js";
5
5
  import {
6
6
  readLockFile,
@@ -231,4 +231,4 @@ ${lines}
231
231
  export {
232
232
  runLockCommand
233
233
  };
234
- //# sourceMappingURL=lock-commands-6U7UIJGR.js.map
234
+ //# sourceMappingURL=lock-commands-4TX3BONI.js.map
package/dist/node.js CHANGED
@@ -108,7 +108,7 @@ import {
108
108
  run,
109
109
  writeArtifact,
110
110
  writeBundle
111
- } from "./chunk-RLI6XD4O.js";
111
+ } from "./chunk-LISUHLWI.js";
112
112
  import "./chunk-7WRJPKGK.js";
113
113
  import {
114
114
  readLockFile,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  assertBundleInput,
3
3
  loadBundleText
4
- } from "./chunk-OONT4ZL4.js";
5
- import "./chunk-RLI6XD4O.js";
4
+ } from "./chunk-UXPDQFG2.js";
5
+ import "./chunk-LISUHLWI.js";
6
6
  import "./chunk-7WRJPKGK.js";
7
7
  import "./chunk-3LQGF2WS.js";
8
8
  import "./chunk-Z2ZIE5CO.js";
@@ -121,4 +121,4 @@ function fmt(v) {
121
121
  export {
122
122
  runPreflightCommand
123
123
  };
124
- //# sourceMappingURL=preflight-command-UYE7SUQV.js.map
124
+ //# sourceMappingURL=preflight-command-MTYGPNVT.js.map
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  assertBundleInput,
8
8
  loadBundleText
9
- } from "./chunk-OONT4ZL4.js";
9
+ } from "./chunk-UXPDQFG2.js";
10
10
  import {
11
11
  sharedSchemaChanges
12
12
  } from "./chunk-AOFFKSJC.js";
@@ -37,7 +37,7 @@ import "./chunk-NDP7OUPS.js";
37
37
  import "./chunk-AE3PDSDS.js";
38
38
  import {
39
39
  resolveLockPath
40
- } from "./chunk-RLI6XD4O.js";
40
+ } from "./chunk-LISUHLWI.js";
41
41
  import "./chunk-7WRJPKGK.js";
42
42
  import {
43
43
  readLockFile
@@ -447,7 +447,7 @@ async function runReleaseCommand(args) {
447
447
  { withSeed: req.records === true }
448
448
  );
449
449
  if (args.static !== void 0) {
450
- const { assertNoSeedLeaks } = await import("./deploy-command-CGVKRWUE.js");
450
+ const { assertNoSeedLeaks } = await import("./deploy-command-VBB7JYXH.js");
451
451
  await assertNoSeedLeaks(args.static, nonPublicSeedValues, args.allowSeedInStatic);
452
452
  }
453
453
  const workspaceId = auth.workspaceId;
@@ -589,7 +589,7 @@ Retry, or drop \`--backup-branch\` to release without one.`,
589
589
  ...backupLabel !== void 0 ? { backupBranch: backupLabel } : {}
590
590
  };
591
591
  if (args.static !== void 0) {
592
- const { buildStaticEnv, deployStaticTo } = await import("./deploy-command-CGVKRWUE.js");
592
+ const { buildStaticEnv, deployStaticTo } = await import("./deploy-command-VBB7JYXH.js");
593
593
  const env = buildStaticEnv(auth.instance, args.staticEnv);
594
594
  const explicit = Object.keys(args.staticEnv).length > 0;
595
595
  summary.static = await deployStaticTo(
@@ -608,4 +608,4 @@ Retry, or drop \`--backup-branch\` to release without one.`,
608
608
  export {
609
609
  runReleaseCommand
610
610
  };
611
- //# sourceMappingURL=release-command-CMMYT6XK.js.map
611
+ //# sourceMappingURL=release-command-NXCPQDFF.js.map
@@ -3,12 +3,12 @@ import {
3
3
  } from "./chunk-ERQZFWIW.js";
4
4
  import {
5
5
  refreshAgentFiles
6
- } from "./chunk-2VTJSI6X.js";
6
+ } from "./chunk-F3MRG7NI.js";
7
7
  import {
8
8
  isMachineOutput,
9
9
  writeJson
10
10
  } from "./chunk-NUQCEOKA.js";
11
- import "./chunk-VK26K7AY.js";
11
+ import "./chunk-XBIFQWZM.js";
12
12
  import {
13
13
  runNpm
14
14
  } from "./chunk-ZSYZTGJH.js";
@@ -21,8 +21,8 @@ import {
21
21
  registryUrl,
22
22
  suppressUpdateNotice,
23
23
  upgradeCommand
24
- } from "./chunk-2AY3PKF4.js";
25
- import "./chunk-RLI6XD4O.js";
24
+ } from "./chunk-CII2TOE4.js";
25
+ import "./chunk-LISUHLWI.js";
26
26
  import "./chunk-7WRJPKGK.js";
27
27
  import "./chunk-3LQGF2WS.js";
28
28
  import "./chunk-Z2ZIE5CO.js";
@@ -175,4 +175,4 @@ function messageOf(err) {
175
175
  export {
176
176
  runUpgradeCommand
177
177
  };
178
- //# sourceMappingURL=upgrade-command-A75DOIUH.js.map
178
+ //# sourceMappingURL=upgrade-command-GSH3M3N6.js.map
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-NUQCEOKA.js";
12
12
  import {
13
13
  fetchWorkspaceBundle
14
- } from "./chunk-SS2V2QOG.js";
14
+ } from "./chunk-UQMMFW32.js";
15
15
  import "./chunk-RQ3FXV4K.js";
16
16
  import "./chunk-K5IOND4K.js";
17
17
  import {
@@ -26,8 +26,8 @@ import {
26
26
  fetchOrExplain,
27
27
  httpFailure
28
28
  } from "./chunk-NDP7OUPS.js";
29
- import "./chunk-S3DOJOW4.js";
30
- import "./chunk-VK26K7AY.js";
29
+ import "./chunk-ORZINVGQ.js";
30
+ import "./chunk-XBIFQWZM.js";
31
31
  import "./chunk-ZSYZTGJH.js";
32
32
  import "./chunk-YX22LKQE.js";
33
33
  import "./chunk-6VNRMKFJ.js";
@@ -37,7 +37,7 @@ import "./chunk-BYQHCCYU.js";
37
37
  import "./chunk-EHP3WPEG.js";
38
38
  import "./chunk-AE3PDSDS.js";
39
39
  import "./chunk-DCMANKMX.js";
40
- import "./chunk-RLI6XD4O.js";
40
+ import "./chunk-LISUHLWI.js";
41
41
  import "./chunk-7WRJPKGK.js";
42
42
  import "./chunk-3LQGF2WS.js";
43
43
  import {
@@ -160,4 +160,4 @@ async function runExport(args) {
160
160
  export {
161
161
  runWorkspaceCommand
162
162
  };
163
- //# sourceMappingURL=workspace-command-YELP47SJ.js.map
163
+ //# sourceMappingURL=workspace-command-XUOPLVQ4.js.map
package/guides/README.md CHANGED
@@ -23,7 +23,7 @@ These guides are for humans. Agents read a different set, generated from the SDK
23
23
  sources so it cannot drift from what the code does:
24
24
 
25
25
  - **`llms.txt`** — a small always-read router: the mental model, the deploy contract, and
26
- every gotcha, ending in a map of the topic files.
26
+ every cross-cutting gotcha, ending in a map of the topic files.
27
27
  - **`llms/*.md`** — one file per surface, opened only when that surface is in play.
28
28
  - **`manifest.json`** — the exhaustive per-entry catalog, reached by targeted lookup.
29
29
 
@@ -53,8 +53,9 @@ can learn the SDK without reading source:
53
53
  --help` and the `cli` array in `manifest.json` do, from the same registry that generates
54
54
  the shell completions. The written version is the [CLI guide](cli.md).
55
55
  - **`llms/*.md`** — one file per surface (object def shapes, statements, values, fields,
56
- filters, lambda bodies, and the legacy names a pulled workspace carries), read only when
57
- that surface is in play. **`llms-full.txt`** is all
56
+ filters, lambda bodies, what a client pays to import a def, an index of exact error
57
+ strings, and the legacy names a pulled workspace carries), read only when that surface
58
+ is in play. **`llms-full.txt`** is all
58
59
  of them concatenated, for a reader that wants one file rather than two.
59
60
  - **`manifest.json`** — the exhaustive reference tier, reached by targeted lookup (grep or
60
61
  `jq` one entry; never read it whole). Every object kind (factory, `Xano.register*` method,
package/llms/client.md ADDED
@@ -0,0 +1,44 @@
1
+ # Consuming defs from a client
2
+
3
+ > Read when a frontend imports a def (`getPath()`/`InferResponse`) or calls the deployed backend — what the import costs, `window.XANO_HOST`, local dev, file URLs, and a Node spot-check.
4
+
5
+ Importing a def into a browser bundle or a Node script — for `getPath()`, `getUrl()`, `verb`, or `InferResponse` — runs its factory calls; these are the costs and the checks.
6
+
7
+ - **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
8
+ no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
9
+ `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
10
+ the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
11
+ ⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
12
+ never removes it. The floor is the RUNTIME, not the def: a second or much richer def
13
+ adds ~2 kB, so trimming a def does not shrink it.
14
+ Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
15
+ plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`
16
+ `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
17
+ URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
18
+ - **Verifying a def outside a bundler.** Inside a bundler (Vite/webpack) importing a
19
+ query def to read `getPath()`/`verb` works directly. To spot-check from Node, run a REAL
20
+ file with `tsx <file.ts>` **from inside the project root** — not `tsx -e "import …"`
21
+ (its CJS-preparse mis-resolves the package `exports` map → ERR_PACKAGE_PATH_NOT_EXPORTED),
22
+ and not bare `node file.ts` (chokes on the `.js`-specifier intra-workspace imports the
23
+ xanots CLI's own loader resolves). Running from outside the project root also breaks
24
+ the `@xanots/sdk` specifier resolution.
25
+
26
+ **Reading the injected backend URL.** `xanots deploy <entry> --static <dir>` writes the deployed env's URL
27
+ into every html document: a prerendered build serves a different one per
28
+ route, and a route without the global renders fine while every call goes to the wrong
29
+ origin. Read it at runtime with a build-time fallback:
30
+ const HOST = (typeof window !== 'undefined' && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
31
+ In LOCAL DEV there is no injected global, so the fallback is what answers: set
32
+ `VITE_XANO_HOST` in a `.env.local` beside `.env.example` at the PROJECT ROOT. The
33
+ scaffold's vite config sets `envDir` there (its `root` is `frontend/`, and Vite
34
+ resolves `.env` files against `root`) — without it the var reads as undefined, the
35
+ host falls back to '', and every call 404s off the dev server.
36
+ ⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
37
+ by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
38
+ bare `XANO_HOST` token.
39
+ **Displaying a stored file.** A file column comes back as `{ path, name, type, size,
40
+ meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
41
+ addresses the instance host WITHOUT the `/tenant/<name>` segment and 404s, silently —
42
+ as a broken `<img>`, with every API assertion still passing. Build the URL from `path`
43
+ instead: `fileUrl(row.avatar, HOST)` returns `null` for
44
+ an absent file and is correct on an ephemeral and an instance workspace alike.
package/llms/errors.md ADDED
@@ -0,0 +1,33 @@
1
+ # Error index
2
+
3
+ > Read when a request, test, build, or pull fails with a message you did not write — the string, its cause, and where the fix is.
4
+
5
+ ### Request-time (deployed) failures
6
+
7
+ - `Unable to locate input: <name>` — `inp()` names an undeclared input, or an attached middleware reads its own `input` (never bound). Declare it; a stack value is `ref("var.field")`; middleware reads the body with `s.util.get_all_input`. `llms.txt`; `llms/kinds-core.md`.
8
+ - `Unable to locate var: <a.b>` (HTTP 500) — a dotted `ref` into a null base (a `db.get` miss), or a column absent from the row (`f.password` is `internal`). Guard existence first, or use the null-safe `safe` option; name the column in `output`. `llms.txt`.
9
+ - `Missing var entry: <name>` inside `s.expect.to_throw` — the body runs in an isolated var stack; bind what it needs inside the body. `llms/tests.md`.
10
+ - `Missing param: field_value` (HTTP 400) — a `db.*` match argument resolved to `null` (a safe ref, a nullable foreign key). Guard existence before the lookup; an optional FK stores `0` and is read with field-match `db.get`. `llms.txt`; `llms/fields.md`.
11
+ - `Value is less than the minimum value of 1` (HTTP 400) — `db.get_by_id` given `0`; use field-match `db.get`. `llms/statements-data.md`.
12
+ - `Invalid op: <op>` — a `cmp` operator (`in`, `like`, …) in a RUNTIME condition, which takes the `expr` set only; write `or(expr(...), expr(...))`. `llms/kinds-core.md`.
13
+ - `Unsupported param format` / `Unsupported parameter reference` — an aggregate or `eval` `name` that is not a bare column, or a bare `eval` name for a joined column. `llms/statements-data.md`.
14
+ - `ParseError: Invalid value for param` (HTTP 400) — `contains`/`@>`/`overlaps` on a text column (use `includes`), or a joined column qualified without `tableAlias`. `llms/statements-data.md`.
15
+ - `ERROR_FATAL: <Type> does not exist: <type>:<n>` — `s.api.call`/`s.task.call`/`s.trigger.call`/`s.workflow_test.call` outside a workflow test; share logic via `defineFunction` + `s.function.run`. `llms.txt`.
16
+ - `ERROR_FATAL "Unable to decode."` — a populated JSON string where the engine expects `c.obj`'s form; write `c.obj({...})` or `obj({...})`. `llms/values.md`.
17
+ - `Precondition failed.` in place of your message — `error` was a bare string; pass `c.text("…")`. `llms/fields.md`.
18
+ - `Param: token - Text filter requires an integer, float, string or boolean value` — `s.api.call` `auth.token` given a tagged `Value`; it must be a bare string. `llms/statements-calls.md`.
19
+ - Every `s.db.add` into one table 400s naming a column while complaining about its VALUE — the column is named `run`. `llms.txt`.
20
+ - HTTP 200 with an EMPTY body where `0` was expected — a bare `returnType: "count"` of zero; wrap it as `{ count: ref("n") }`. `llms/statements-data.md`.
21
+ - A lambda returns error TEXT with HTTP 200 (`Could not resolve "node:crypto"`, an undefined-binding message) — the body threw; use the preloaded globals, the surface's bindings, and `capture`. `llms/lambda.md`.
22
+ - `Unable to locate request.` (404) on an endpoint that deployed — a lowercase verb, a `.` in the name, or a CORS `mode` typo that dropped the whole API group. `llms/kinds-core.md`.
23
+ - A browser call fails on a missing `access-control-allow-origin` while deploy looks fine — CORS `mode: "custom"` with `"*"` or an unmatched origin. `llms/kinds-core.md`.
24
+ - A websocket is closed right after the handshake — a refused `connect` gate: an empty/falsy return, a crash, or a gating trigger with no `response`. `llms/triggers.md`.
25
+ - `to_throw failed - response is ok` around an auth-refused call — `ERROR_CODE_ACCESS_DENIED` carries no message; the auth gate is not reachable from a workflow test. `llms/tests.md`.
26
+ - Output reads `Hi [object Object]` — a tagged value inside a JS template literal; compose at runtime with `withFilters` + `fl.concat`. `llms.txt`.
27
+
28
+ ### Build-time and tooling failures
29
+
30
+ - `must be ES modules` — package.json says `"type": "commonjs"`; set `"type": "module"`. `llms.txt` Quickstart.
31
+ - A response types as `StackTupleWidened` — a `Statement[]` helper was spread into the stack; return `statements(...)`. `llms.txt`; `llms/statements-runtime.md`.
32
+ - `ERR_PACKAGE_PATH_NOT_EXPORTED` — `tsx -e "import …"`, or running from outside the project root; run a real file with `tsx <file.ts>` from the root. `llms/client.md`.
33
+ - `Missing statement: mvp:placeholder` on import — an unconfigured slot in a pulled tree; replace it with the statement it stands in for. `llms/legacy.md`.
package/llms/fields.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Column and input types
2
2
 
3
- > Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and accessor methods, and the `s.precondition` error/status contract that rides the same catalog.
3
+ > Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and methods, and the `s.precondition` error/status contract. Also `table({ seed })` rows and `use_xdo`.
4
4
 
5
5
  Author table columns + function/API inputs with the typed catalog: `f.<type>(opts?)`
6
6
  for columns, `input.<type>(opts?)` for inputs. Common opts: `required`, `nullable`,
@@ -45,6 +45,31 @@ any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embe
45
45
  queries it.
46
46
  `{ array: true }` makes any `f.*` scalar a **list column** — `f.text({ array: true })`
47
47
  surfaces as `string[]` in `InferRow<typeof table>` (the column analogue of `input.list`).
48
+ **Seed a table's starting rows with `table({ seed })`.** `seed` takes rows
49
+ typed against the table's schema as a WRITE shape (a column without
50
+ `required: true`, and the system columns, may be omitted; `null` needs
51
+ `nullable: true`) — inline (`seed: [{ name: "…" }]`), a FILE
52
+ (`seed: seedFile("./seed.json", import.meta.url)`; path resolves against the DECLARING
53
+ file), or a thunk (`seed: () => import("./seed.json")`, async ok, `.default` unwrapped).
54
+ Inline rows are TYPED against the schema at compile time; a `seedFile`/thunk seed is
55
+ NOT — the JSON is opaque to the typechecker, and the `xanots export`/`deploy` CLI
56
+ validates it instead, naming the row index, the offending column, and the known
57
+ columns. ⚠ Prefer `seedFile` for a file: a thunk's `import()` sits in
58
+ YOUR module, so a bundler emits the JSON as a served chunk. NEITHER is a secrecy
59
+ boundary — a bundler that inlines the asset ships them either way. Never put secrets
60
+ in `seed`. `deploy --static` REFUSES a build carrying internal/sensitive seed values;
61
+ `--allow-seed-in-static` overrides for deliberately public demo credentials.
62
+ Deploy is a full replace, so re-deploying re-seeds
63
+ cleanly (no duplication). Omit `id` and rows auto-number `1..N` (int PK) or take
64
+ a stable derived uuid (uuid PK); supplying `id` pins it (engine preserves it,
65
+ resets an int sequence past the max). All-or-nothing — mixing explicit and
66
+ omitted `id` throws. A `system:false` PK is the author's to supply. Pinning is
67
+ `seed`-only — `s.db.bulk.add` DROPS `id` unless `allowIdField: true`.
68
+ **`use_xdo` storage mode.** Workspace setting (`registerWorkspace({ use_xdo })`,
69
+ default `false`) controlling whether fields are stored as JSON under the `xdo`
70
+ column (`true`, adds a `gin(xdo)` index) or as real columns (`false`, no gin).
71
+ Tables inherit it; override per-table with `table({ useXdo })`. Resolved at
72
+ `export()`, so the workspace and tables can be registered in any order.
48
73
  A **column `default` must stay within the BMP** — a 4-byte character (codepoint > U+FFFF,
49
74
  e.g. an emoji) is mangled into invalid UTF-8 by the engine's default pipeline and is rejected
50
75
  at export rather than 500ing at deploy (Postgres `22021`); BMP defaults (accents, `€`, most
package/llms/filters.md CHANGED
@@ -38,14 +38,10 @@ statement, and one CANNOT be synthesized in the SDK (it would compile to this sa
38
38
  database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHERE …`),
39
39
  which the DB applies atomically. Reserve the pipeline form for low-contention counters
40
40
  where a rare lost update is acceptable.
41
- ⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
42
- does NOT expose: the engine derives it from ids assigned at import not knowable from a
43
- `table()` def (identity is a name + guid, not the numeric id) — and `sql_name` persists
44
- empty. The derived name is also NOT STABLE: a deploy is a full replace, so every table is
45
- created afresh and the id in its name moves each time, on the same unchanged project. So
46
- the safe counter drops out of the typed surface: resolve the physical name from
47
- `information_schema` inside the request that uses it, and never store, cache or hardcode
48
- one. A typed atomic path needs an engine change.
41
+ ⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface does NOT
42
+ expose and which is NOT STABLE across deploysresolve it from `information_schema` inside
43
+ the request that uses it, never store or hardcode one (rules under `s.db.direct_query` in
44
+ `llms/statements-data.md`). A typed atomic path needs an engine change.
49
45
 
50
46
  - `fl.add(value: decimal): decimal`
51
47
  - `fl.append(value: <T>, path: text): <T>[]`
@@ -10,3 +10,4 @@
10
10
  - `output?`: `{ schema: Record<string, input.*>, enabled? }` — structured-output schema. `schema` is a named-field record authored with the `input.*` catalog, exactly like a `defineFunction`/`query` `input:` map (the stored `structuredOutputsSchema` is the same wire shape as function inputs). e.g. `output: { schema: { priority: input.enum(["low","high"]), summary: input.text() } }`. When you pass the agent *handle* to `s.ai.agent.run({ agent })`, `.result` is typed straight from this schema — no `resultShape` witness needed (the shape is declared once). `resultShape` remains only to override that, or to type an agent referenced by bare name.
11
11
  - **Run + read recipe (`s.ai.agent.run`):** bind the run to a var (`s.ai.agent.run({ agent, args, as: "run" })`) — it produces a rich envelope, and the completion is at **`.result`**. Read one structured field with a dotted ref (`response: ref("run.result.priority")`) or the whole completion (`ref("run.result")`, typed from `output.schema`); persist it in a later step the same way (`s.set_var({ name, value: ref("run.result.summary") })`). `args` is a plain object of run inputs (`{ topic: inp("topic") }` — raw literals are fine, e.g. `{ max_steps: 3 }`) surfaced to the agent as `{{ $args.topic }}`. Tool-call / per-step data (when tools ran) rides `.toolCalls` / `.steps` on the same envelope — both optional, absent or empty when no tools executed.
12
12
  - **Marketplace:** for chat history, use `@xanots/chatbot`; for vector RAG, use `@xanots/vector` (`tools: [vector.searchTool]`). Find add-ons with `xanots marketplace search <q>`.
13
+ - Both kinds persist under the `toolset` payload key, so a same-name `agent` + `mcpServer` pair derives ONE guid and `export()` throws — give them distinct names.
@@ -13,8 +13,18 @@ to survive a rename).
13
13
  - `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
14
14
  - `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
15
15
  - `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
16
- - `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param. Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
17
- - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing `%2e` counts addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
16
+ - `name` is the endpoint PATH within the group.
17
+ - A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input.
18
+ - Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing.
19
+ - A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked).
20
+ - The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param.
21
+ - Inputs absent from the path are ordinary query-string/body params.
22
+ - Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
23
+ - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand.
24
+ - `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint).
25
+ - The keys are typed from the literal `name`, so a typo is a compile error.
26
+ - The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
27
+ - What importing a def costs a browser bundle, the `routes.gen.ts` alternative, and how to spot-check a def from Node: `llms/client.md`.
18
28
  - `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
19
29
  - `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
20
30
  - `mode?`: `"default"` (the default) | `"custom"` | `"disabled"`, lowercase — a fourth value THROWS at export (`apiGroup()` itself does not check), because Xano neither rejects nor blanks it: it DROPS THE WHOLE API GROUP on import, so the deploy succeeds and every query in the group 404s. ⚠ Every OTHER field applies only under `"custom"`: `"default"` serves a FIXED permissive policy (any origin, `allow-headers: *`, `allow-credentials: true`, `max-age: 86400`) and ignores the block, so setting `maxAge`/`allowCredentials`/`allowHeaders` alone changes nothing. `"disabled"` sends no CORS headers at all, so every browser call fails.
@@ -66,4 +76,8 @@ surface — `s.conditional`/`s.while` `when` (incl. each `elif` branch), and
66
76
  `ambiguous-condition`.
67
77
  - A **filtered** operand (`withFilters(...)`) works inline in any condition/`where` (conditional,
68
78
  while, `db.query`/addon, …) — e.g. `cmp(withFilters(col("title"), fl.trim()), "=", inp("q"))`.
79
+ - **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
80
+ number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
81
+ container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
82
+ folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.
69
83
  - e.g. `db.query({ table: posts, where: expr(col("author"), "=", auth("id")), as: "rows" })`.
@@ -1,6 +1,6 @@
1
1
  # Knowledge def shape
2
2
 
3
- > Read when the workspace defines a `knowledge()` item the markdown its AI agents read.
3
+ > Read when the workspace defines a `knowledge()` item for its AI agents.
4
4
 
5
5
  - `knowledge({ name, description?, type?, mode?, enabled?, body, refs?, guid?, tags? })` — markdown the workspace's AI agents read before they act. Takes `registerKnowledge`.
6
6
  - `body` (REQUIRED): `knowledgeFile("./runbook.md", import.meta.url)` — a path to a real markdown FILE. There is no inline-string form. The path resolves relative to the MODULE THAT DECLARES the item, not the process working directory and not the workspace entry; `import.meta.url` is required and is what makes that true.
@@ -59,7 +59,7 @@
59
59
  - **Conversation frames — the transcript hydrates the client, so DO NOT build a hydration endpoint.**
60
60
  - On a `conversation` channel the replay is PUSHED automatically at join, unasked: `conversation_start` (`payload.count`) → the last `limit` messages, each a normal `action: "message"` frame carrying its ORIGINAL `type` and `payload` plus `conversation: true` and the original `ts` → `conversation_end`.
61
61
  - So the client needs NO fetch, no `GET /messages`, and no table read to paint the initial view. Render `message` frames identically either way; the backfill paints itself.
62
- - ⚠ `{ enabled: true }` ALONE IS A NO-OP: `limit` defaults to 0, and 0 means RETAIN NONE (not retain everything), so the transcript is never written and never replayed, with no error. ALWAYS PASS `limit`.
62
+ - ⚠ `{ enabled: true }` alone is a no-op: `limit` defaults to 0 and 0 means RETAIN NONE (see `conversation` above) always pass `limit`.
63
63
  - The POST-HANDLER broadcast payload IS the stored transcript row — a handler must broadcast everything the UI needs to render a past message (author name, id, `created_at`). Nothing else is replayed.
64
64
  - Only `deliverTo` `"channel"`/`"others"` are RECORDED, so a `"sender"` response is invisible to every future joiner by construction.
65
65
  - The transcript is a capped ring (`limit`, `ttl`), not storage. Persist to a table only for durability, search, or reads BEYOND that window — never merely to hydrate a joiner.
package/llms/lambda.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lambda bodies (JavaScript)
2
2
 
3
- > Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`. Lambda workers are a bounded shared resource, and each surface binds a different set of identifiers.
3
+ > Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`; each surface binds a different set of identifiers.
4
4
 
5
5
  **A lambda is an escape hatch, not a default.** The body runs outside the request's own
6
6
  runtime, and a workspace has a BOUNDED pool of lambda workers every lambda in it shares
package/llms/legacy.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Legacy paradigms and retired statements
2
2
 
3
- > Read when the code was PULLED from an existing Xano instance rather than authored here — how a codegen'd tree reads and the three shapes in it that must not be "fixed", plus a `raw({ name: "mvp:…" })` statement, a `realtimeTrigger()`, or any name the catalogs do not list.
3
+ > Read when the code was PULLED from an existing Xano instance — how a codegen'd tree reads, the shapes in it that must not be "fixed", and any `raw({ name: "mvp:…" })`, `realtimeTrigger()`, or name the catalogs do not list.
4
4
 
5
5
  Older paradigms this SDK still supports and still emits when it decodes an existing
6
6
  workspace. **Do not author these.** They are listed by name only so you recognize them
@@ -1,6 +1,6 @@
1
1
  # Object kinds
2
2
 
3
- > Read when deciding WHAT to build — every authorable primitive (tables, endpoints, functions, tasks, agents, MCP, realtime, microservices, …), what each is, and which factory + register method build it. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
3
+ > Read when deciding WHAT to build — every authorable primitive, what each is, and which factory + register method builds it. Also when `register*` will not typecheck an array built by `.flatMap()`/`.concat()`.
4
4
 
5
5
  Author with the factory, register on the Xano instance, lands under the payload key. Each line ends with a one-liner on what the primitive is.
6
6
 
@@ -21,7 +21,7 @@ Author with the factory, register on the Xano instance, lands under the payload
21
21
  - mcp_server: `mcpServer` → `Xano.registerMcpServers` → payload `toolset` — An MCP server exposing a set of tools to external MCP clients.
22
22
  - agent: `agent` → `Xano.registerAgents` → payload `toolset` — An AI agent: an LLM configuration plus the tools it can call. Invoke it from any stack (query/function/task/tool/trigger) with `s.ai.agent.run` — no public endpoint; the result is a rich envelope whose completion text is at `.result`.
23
23
  - task: `task` → `Xano.registerTasks` → payload `task` — A scheduled background job (cron/interval) that runs a stack on a timer.
24
- - workflow_test: `workflowTest` → `Xano.registerWorkflowTests` → payload `workflow_test` — An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one makes the engine CLONE that datasource before every run, so pointing a test at production-sized data can be slow enough to fail the run — `"live"` warns at compile time.
24
+ - workflow_test: `workflowTest` → `Xano.registerWorkflowTests` → payload `workflow_test` — An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one is the clone trap described in `llms/kinds-core.md`.
25
25
  - middleware: `middleware` → `Xano.registerMiddleware` → payload `middleware` — A reusable pre/post stack attached to a query/function/task/tool/API group to run before or after its own logic.
26
26
  - addon: `addon` → `Xano.registerAddons` → payload `addon` — A reusable read fragment that enriches a query result by joining related table data.
27
27
  - realtime_server: `realtimeServer` → `Xano.registerRealtimeServers` → payload `realtime_server` — A realtime (websocket) server: the canonical-addressed container that owns realtime channels. Off until `enabled: true`. Returns a handle with `getUrl(baseUrl)`/`getPath()` for the client's socket URL (`wss://<host>/ws/<canonical>`).
@@ -13,8 +13,20 @@ Auth & calls:
13
13
  - `s.security.create_guid({ as? })` — bind a fresh GUID string. Takes nothing else.
14
14
  - `s.function.run({ fn, input?, as?, runtime? })` / `s.function.call({ fn, input?, as? })` — run another function; `input` is keyed by the target's input names.
15
15
  - `runtime?` runs it in the BACKGROUND: `{ mode: "async-shared" }` or `{ mode: "async-dedicated", cpu?, memory?, timeout?, maxRetry? }` (resources read at dedicated only). An async call DOES NOT return the result — it dispatches and continues, so `as` binds nothing; collect with `s.await({ ids })`. Omit for a normal call. Same block on `s.ai.agent.run`.
16
- - `s.api.call({ api, input?, headers?, auth?, as? })` — invoke an endpoint. `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) — a bare name is refused, because a query's identity is composed from its api group, verb, and name. `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s — a `{ "Name": value }` record (values may be tagged), a `string[]` of `"Name: value"` lines, or one `Value`. `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING — a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it. Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`) — elsewhere it deploys clean and 500s the first real request.
17
- - `s.api.request({ url, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`: `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH), `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter — the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (→ query string for GET/HEAD/OPTIONS, body otherwise), `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines prefer a header over a `?key=` query param for a credential — a URL travels into access logs, proxies and `Referer`. ⚠ Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request — read `response.result`. A NAME outside the header-token charset is refused, and a LITERAL value carrying a newline is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs lead the emitted array and computed ones follow, so the wire order is not the record's key order, `timeout` a `number` in seconds (1–86400), and `follow_location`/`verify_host`/`verify_peer` booleans. `description` (Settings tab) and `output` filters (Output tab) ride the envelope. SSL cert interdependencies (certificate↔private_key, ca_certificate→verify_peer) are checked at build time when statically provable, else by the engine at runtime. The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.
16
+ - `s.api.call({ api, input?, headers?, auth?, as? })` — invoke an endpoint. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`).
17
+ - `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) — a bare name is refused, because a query's identity is composed from its api group, verb, and name.
18
+ - `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s — a `{ "Name": value }` record (values may be tagged), a `string[]` of `"Name: value"` lines, or one `Value`.
19
+ - `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING — a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it.
20
+ - Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees.
21
+ - `s.api.request({ url, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` — external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`:
22
+ - `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH).
23
+ - `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter — the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (→ query string for GET/HEAD/OPTIONS, body otherwise).
24
+ - `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines — prefer a header over a `?key=` query param for a credential — a URL travels into access logs, proxies and `Referer`.
25
+ - ⚠ Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request — read `response.result`.
26
+ - A NAME outside the header-token charset is refused, and a LITERAL value carrying a newline is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs lead the emitted array and computed ones follow, so the wire order is not the record's key order.
27
+ - `timeout` a `number` in seconds (1–86400), and `follow_location`/`verify_host`/`verify_peer` booleans.
28
+ - `description` (Settings tab) and `output` filters (Output tab) ride the envelope. SSL cert interdependencies (certificate↔private_key, ca_certificate→verify_peer) are checked at build time when statically provable, else by the engine at runtime.
29
+ - The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.
18
30
  - `s.stream.from_request({ url, method?, …tls, as? })` — streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope). `url` is REQUIRED — it shares `api.request`'s engine declaration, which has no default.
19
31
  - `s.util.send_email({ to, subject, message, from?, cc?, bcc?, reply_to?, service_provider?, api_key?, scheduled_at?, as? })` — send email from the stack. `service_provider` is `"xano"` (the built-in mailer — needs NO `api_key` and no configuration, and does not require a verified sender) or `"resend"` (pass the key as `api_key: env("RESEND_API_KEY")`). Prefer this over hand-rolling `s.api.request` against a mail provider.
20
32
  - `s.webflow.request({ path, method?, …tls, as? })` — Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied), and `path` is REQUIRED — the engine rejects an empty one. No `headers`: the engine builds its own from the workspace's Webflow connection and ignores an authored value.
@@ -25,7 +37,16 @@ Auth & calls:
25
37
 
26
38
  Microservices (the `microservice()` def and the statement that calls it):
27
39
 
28
- - `microservice({ name, kind, … })` — two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` — passing both throws. EARLY SURFACE, expected to change — every export of a workspace declaring one prints a notice saying so. `configs`/`volumes` are typed and `@deprecated` but NOT deployable: the engine rejects an import carrying either, so `export()` fails the build rather than letting the deploy fatal. Put a value the workload reads in a container's `env`, and storage in a container's own `volumes` (`emptyDir`/`persistent`/`config`). Container names are free-form — they need not match the microservice name, which is what a stack addresses.
40
+ - `microservice({ name, kind, … })` — two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` — passing both throws.
41
+ - EARLY SURFACE, expected to change — every export of a workspace declaring one prints a notice saying so.
42
+ - `configs`/`volumes` are typed and `@deprecated` but NOT deployable: the engine rejects an import carrying either, so `export()` fails the build rather than letting the deploy fatal.
43
+ - Put a value the workload reads in a container's `env`, and storage in a container's own `volumes` (`emptyDir`/`persistent`/`config`).
44
+ - Container names are free-form — they need not match the microservice name, which is what a stack addresses.
29
45
  - SECRETS RIDE ALONG — `chart.values` and `registryAuth.dockerconfigjson` are carried into the bundle, and into a pulled tree, verbatim (they must be, or a pulled microservice could not be redeployed). Both are stored strings with NO deploy-time indirection: `process.env.X` in the def resolves at EXPORT and writes the literal into the bundle, so it is not a way to keep the credential out. Either leave `registryAuth` unset (public image, or a credential attached outside this workspace) or treat the bundle and any pulled tree as secret material — keep them out of git, or rotate after. Export prints a notice per microservice carrying either field; `--strict` does not promote it. For a secret a STACK reads, the mapped surface is `workspaceConfig({ env })` + `env("NAME")`.
30
- - `s.microservice.request({ host, path, port?, method?, params?, headers?, timeout?, follow_location?, as? })` — in-cluster microservice call (`mvp:microservice_request`); no TLS fields. ONLY `host`+`path` required; the rest default to the engine's values (`GET`/`{}`/`[]`/`10`/`true`), always emitted. Pass the `microservice()` DEF as `host` — it binds by NAME (how the engine resolves it), so a rename fixes every call site and the port is checked before deploy. `port?` folds into `host` as `"name:port"`: a def exposing ONE `servicePort` resolves automatically, SEVERAL requires it. A raw `"name:port"` string works, unvalidated, and is the only way to reach an instance-level microservice. `tenantDeploy: "manual"` on the def imports the row without starting the workload.
46
+ - `s.microservice.request({ host, path, port?, method?, params?, headers?, timeout?, follow_location?, as? })` — in-cluster microservice call (`mvp:microservice_request`); no TLS fields.
47
+ - ONLY `host`+`path` required; the rest default to the engine's values (`GET`/`{}`/`[]`/`10`/`true`), always emitted.
48
+ - Pass the `microservice()` DEF as `host` — it binds by NAME (how the engine resolves it), so a rename fixes every call site and the port is checked before deploy.
49
+ - `port?` folds into `host` as `"name:port"`: a def exposing ONE `servicePort` resolves automatically, SEVERAL requires it.
50
+ - A raw `"name:port"` string works, unvalidated, and is the only way to reach an instance-level microservice.
51
+ - `tenantDeploy: "manual"` on the def imports the row without starting the workload.
31
52
  - `s.workflow_test.call({ workflowTest, datasource?, as? })` — run another workflow test from inside one, which is the only place it runs (see Gotchas in `llms.txt`). The odd one out: NO `input` (a workflow test takes none), and it carries `datasource?` instead — same clone caveat as the kind's own field.
@@ -1,6 +1,6 @@
1
1
  # Statement catalog
2
2
 
3
- > Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace. Look here after the control-flow core in the router does not cover what you need.
3
+ > Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace.
4
4
 
5
5
  ### (top-level)
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Statement runtime behavior
2
2
 
3
- > Read when you need to know what a statement's `as:` output actually holds, or why a bound variable is not the shape you expected.
3
+ > Read when you need to know what a statement's `as:` output holds, or why a bound variable is not the shape expected.
4
4
 
5
5
  Runtime behavior (what the `as:` output holds, and misses):
6
6
 
@@ -38,11 +38,26 @@ Runtime behavior (what the `as:` output holds, and misses):
38
38
  - Attaching a typed `addon({ table, output })` handle merges its alias (the last `as` segment) onto the row in `InferResponse`: `{cols}` for `single`, `{cols}[]` for `list`, `number` for `count`, `boolean` for `exists`, and for `aggregate` an array keyed by the `group`/`eval` aliases (`unknown` values; `unknown` when neither is declared).
39
39
  - An attachment-level `output` narrows an object/array graft further. A bare-NAME reference grafts `unknown` — narrow it at the call site.
40
40
  - ⚠ An alias that shadows an existing column on the queried table throws at build time; rename with a `_` prefix.
41
- - **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack. Attach with the host's `middleware: { pre, post }` field on `query`/`function`/`task`/`tool`/`apiGroup` (NOT triggers): each phase is an ordered list of middleware refs (def handle or name), or `{ middleware, active: false }` to keep an entry disabled. Providing a phase **overrides** it (sets the stored `pre_customize`/`post_customize` flag); omitting a phase **inherits** the parent tier's chain — the engine resolves Query → API Group → Workspace at request time (override, not merge; the API-Group tier applies to queries — functions/tasks/tools have no API-group binding and inherit straight from the workspace). Prefer a def handle over a bare name when the middleware pins an explicit `guid`. `pre: middleware.clear()` (an empty list) overrides with nothing — stop inheriting. Workspace-level defaults are the terminal tier: `workspaceConfig({ middleware: { query: { pre }, function, task, tool } })` emits the flat `{host}_{phase}` map (no `_customize` flags) — setting it replaces the whole workspace map, so unlisted hosts are cleared; omit the field to leave existing workspace middleware untouched. Distinct from `s.middleware.call` (inline invoke).
42
- - **Middleware request context.** A `pre` middleware runs **after** auth resolution, so `auth()` is available inside the middleware when the host is authenticated (its `auth` names an auth table); on a public host `auth()` is `null`. This matters for the canonical use — a rate limit keyed by `auth("id")`: on an authenticated endpoint the bucket is per-user, but attach the same middleware to a public endpoint and every anonymous caller keys under the same `null` id (one shared bucket), silently. To catch that, `export()` **warns** (never blocks) when a middleware whose stack references `auth()` is directly attached to a host where `auth()` may be null — a `query` with no auth table, a `task` (scheduled, never authenticated), or a `function`/`tool` (whose auth is caller-dependent). An authenticated query (its own `auth` table set) is skipped. The check is direct-attachment only; a middleware reaching a public query via API-group/workspace tier inheritance is not caught.
43
- - **Rate-limit recipe (the canonical middleware).** Per-user rate limiting is the most common middleware. Author it with `s.redis.ratelimit` and a **composite key** built via the filter chain — `"prefix" + auth("id")` does not exist, you build the key: `middleware({ name: "write_rl", exceptionPolicy: "rethrow", stack: [ s.redis.ratelimit({ key: withFilters(c.text("rl:write:"), fl.concat(auth("id"))), max: c.int(10), ttl: c.int(30), error: c.text("Too fast.") }) ] })`. `exceptionPolicy` defaults to `"rethrow"`, which is what makes a tripped limit abort with HTTP 429; `"silent"` would let the over-limit request through. Attach it with `middleware: { pre: [writeRl] }` on an **authenticated** host (its `auth` set) so `auth("id")` keys per-user; on a public host `auth("id")` is null and every caller shares one bucket (`export()` warns — see request context above). **Shared-bucket rule:** co-attaching one middleware object to N hosts means all N share the *same* key ⇒ *one* counter `max: 10` is a global per-user budget across them, not 10-per-host. Vary the key (fold in the host/action name) for an independent limit per host.
41
+ - **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack. Distinct from `s.middleware.call` (inline invoke).
42
+ - Attach with the host's `middleware: { pre, post }` field on `query`/`function`/`task`/`tool`/`apiGroup` (NOT triggers): each phase is an ordered list of middleware refs (def handle or name), or `{ middleware, active: false }` to keep an entry disabled.
43
+ - Providing a phase **overrides** it (sets the stored `pre_customize`/`post_customize` flag); omitting a phase **inherits** the parent tier's chain — the engine resolves Query API Group Workspace at request time (override, not merge; the API-Group tier applies to queriesfunctions/tasks/tools have no API-group binding and inherit straight from the workspace).
44
+ - Prefer a def handle over a bare name when the middleware pins an explicit `guid`.
45
+ - `pre: middleware.clear()` (an empty list) overrides with nothing — stop inheriting.
46
+ - Workspace-level defaults are the terminal tier: `workspaceConfig({ middleware: { query: { pre }, function, task, tool } })` emits the flat `{host}_{phase}` map (no `_customize` flags) — setting it replaces the whole workspace map, so unlisted hosts are cleared; omit the field to leave existing workspace middleware untouched.
47
+ - **Middleware request context.** A `pre` middleware runs **after** auth resolution, so `auth()` is available inside the middleware when the host is authenticated (its `auth` names an auth table); on a public host `auth()` is `null`.
48
+ - This matters for the canonical use — a rate limit keyed by `auth("id")`: on an authenticated endpoint the bucket is per-user, but attach the same middleware to a public endpoint and every anonymous caller keys under the same `null` id (one shared bucket), silently.
49
+ - To catch that, `export()` **warns** (never blocks) when a middleware whose stack references `auth()` is directly attached to a host where `auth()` may be null — a `query` with no auth table, a `task` (scheduled, never authenticated), or a `function`/`tool` (whose auth is caller-dependent). An authenticated query (its own `auth` table set) is skipped.
50
+ - The check is direct-attachment only; a middleware reaching a public query via API-group/workspace tier inheritance is not caught.
51
+ - **Rate-limit recipe (the canonical middleware).** Per-user rate limiting is the most common middleware. Author it with `s.redis.ratelimit` and a **composite key** built via the filter chain — `"prefix" + auth("id")` does not exist, you build the key: `middleware({ name: "write_rl", exceptionPolicy: "rethrow", stack: [ s.redis.ratelimit({ key: withFilters(c.text("rl:write:"), fl.concat(auth("id"))), max: c.int(10), ttl: c.int(30), error: c.text("Too fast.") }) ] })`.
52
+ - `exceptionPolicy` defaults to `"rethrow"`, which is what makes a tripped limit abort with HTTP 429; `"silent"` would let the over-limit request through.
53
+ - Attach it with `middleware: { pre: [writeRl] }` on an **authenticated** host (its `auth` set) so `auth("id")` keys per-user; on a public host `auth("id")` is null and every caller shares one bucket (`export()` warns — see request context above).
54
+ - **Shared-bucket rule:** co-attaching one middleware object to N hosts means all N share the *same* key ⇒ *one* counter — `max: 10` is a global per-user budget across them, not 10-per-host. Vary the key (fold in the host/action name) for an independent limit per host.
44
55
  - **Middleware `exceptionPolicy`** governs what a **throw** in the middleware stack does to the request (XanoTS passes the value through; the Xano engine interprets it). `"rethrow"` is the **default** — the throw aborts the request and surfaces the authored `error`/status (a tripped `s.redis.ratelimit` → HTTP 429); the `post` chain still runs. `"silent"` swallows the throw, so a guard set to it is **not enforced** — advisory middleware only. `"critical"` is `"rethrow"` plus skipping the `post` chain. The only difference between `rethrow` and `critical` is whether `post` runs — no status or logging change.
45
56
 
46
- - **Request history** controls per-object execution capture (the request/task/trigger debugger). Authored as a single scalar `history` field on any primitive: `false` off, `true` on at the default capture depth, a number = capture depth (how many statement executions are recorded per history record — NOT record retention), `"all"` unlimited. **Omit `history` to inherit** — the engine resolves object → container → workspace at request time (a query inherits from its API group, a tool from its toolset envelope, everything else straight from the workspace). Any authored value stops inheriting for that object. Per-kind defaults (when inheriting): query/task/tool capture ON, function/trigger/middleware OFF; default depth 100. Container tiers are authorable too — `apiGroup({ history })` sets the `query_*` default its queries inherit, and an agent/mcp_server/toolset `history` sets the `tool_*` default its tools inherit. Workspace-level defaults are the terminal tier: `workspaceConfig({ history: { query, function, task, tool, trigger, middleware } })` emits the flat `{objType}_enabled`/`{objType}_limit` map (no inherit flag) — setting it is wholesale (unlisted types fall back to their engine default), so declare every default you want to keep; omit the field to leave existing workspace history untouched.
57
+ - **Request history** controls per-object execution capture (the request/task/trigger debugger). Authored as a single scalar `history` field on any primitive: `false` off, `true` on at the default capture depth, a number = capture depth (how many statement executions are recorded per history record — NOT record retention), `"all"` unlimited.
58
+ - **Omit `history` to inherit** — the engine resolves object → container → workspace at request time (a query inherits from its API group, a tool from its toolset envelope, everything else straight from the workspace). Any authored value stops inheriting for that object.
59
+ - Per-kind defaults (when inheriting): query/task/tool capture ON, function/trigger/middleware OFF; default depth 100.
60
+ - Container tiers are authorable too — `apiGroup({ history })` sets the `query_*` default its queries inherit, and an agent/mcp_server/toolset `history` sets the `tool_*` default its tools inherit.
61
+ - Workspace-level defaults are the terminal tier: `workspaceConfig({ history: { query, function, task, tool, trigger, middleware } })` emits the flat `{objType}_enabled`/`{objType}_limit` map (no inherit flag) — setting it is wholesale (unlisted types fall back to their engine default), so declare every default you want to keep; omit the field to leave existing workspace history untouched.
47
62
 
48
63
  - **Workspace environment variables** set a tenant's env vars through the workspace object: `workspaceConfig({ env: { STRIPE_KEY: process.env.STRIPE_KEY!, APP_BASE_URL: "https://…" } })`. Author them as a name→value MAP. Read a var back with `env("NAME")` (→ `$env.NAME`), which compiles to tag "setting" with the plain name. Values are SECRETS: prefer sourcing from `process.env` over committing literals, and don't commit a compiled bundle with real values. `deploy` REPLACES the tenant's env with the declared map; `release` (merge) is ADD-ONLY — it creates missing keys but does NOT update or remove existing ones, so changing a value in code and releasing leaves the live value unchanged. Omit `env` to leave existing env untouched. The separate `settings` field is a plain object.
package/llms/tests.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Saved unit tests, assertions, and mocks
2
2
 
3
- > Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement should mock a value, or when running a deployed environment's tests.
3
+ > Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement needs a `mock`, or when running a deployed environment's tests.
4
4
 
5
5
  Named input sets run against one object, with assertions on its response — the tests the Xano editor shows. NOT `workflowTest()`, which is a standalone object with its own stack that calls other objects.
6
6
 
@@ -40,7 +40,7 @@ A statement returns a value instead of doing its work, for one named test.
40
40
 
41
41
  The run is isolated in ways that make a correct test fail for reasons the failure message does not name.
42
42
 
43
- - The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
43
+ - The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** (same as a unit test, above): a read of seeded row 1 fails with its own precondition message, not an empty-database error. Build fixtures INSIDE the test.
44
44
  - `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
45
45
  - `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. An outer one raises `Missing var entry: <name>` there, which the test reports as `to_throw` not matching (`export()` warns). `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
46
46
  - `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` — which reads as a broken auth gate on a gate that works.