@xanots/sdk 0.0.8 → 0.0.9

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 (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/bin.js +4 -4
  4. package/dist/{chunk-2ROR3AZC.js → chunk-5YQOIFT4.js} +2 -2
  5. package/dist/{chunk-C3M5K4ZH.js → chunk-6Z5CNWFC.js} +2 -2
  6. package/dist/{chunk-7AXQKABT.js → chunk-7OXFMCTX.js} +3 -3
  7. package/dist/{chunk-SNO3KHCY.js → chunk-K3YSQDXJ.js} +32 -18
  8. package/dist/{chunk-PYS7UNNW.js → chunk-KSV7VOEX.js} +2 -2
  9. package/dist/{chunk-EM5QPTXP.js → chunk-L6TPHWAU.js} +7 -7
  10. package/dist/{chunk-CDAQUY5W.js → chunk-OW2QZEWL.js} +2 -2
  11. package/dist/{chunk-64QK6JEK.js → chunk-QONOJO45.js} +36 -2
  12. package/dist/{chunk-FEQTQ6PM.js → chunk-UDL46O35.js} +2 -2
  13. package/dist/{chunk-XP7S3VWY.js → chunk-YNZFIY4J.js} +2 -2
  14. package/dist/{chunk-3MDKV2VA.js → chunk-ZZLPTHT5.js} +2 -2
  15. package/dist/cli.js +3 -3
  16. package/dist/{codegen-command-U2W72DLG.js → codegen-command-YSCSCVT5.js} +8 -8
  17. package/dist/{deploy-command-QZ3GNLAD.js → deploy-command-5TTI4TUP.js} +9 -9
  18. package/dist/{env-target-NAYVEXBH.js → env-target-4T7AT347.js} +2 -2
  19. package/dist/{ephemeral-command-DL7EY4LL.js → ephemeral-command-7TK52YQM.js} +2 -2
  20. package/dist/index.js +5 -5
  21. package/dist/{init-command-4MG3UKUO.js → init-command-OVFFW4QS.js} +5 -5
  22. package/dist/internal.js +18 -12
  23. package/dist/{lock-commands-Y4S4NUCE.js → lock-commands-ET7NIOLH.js} +4 -4
  24. package/dist/node.js +6 -6
  25. package/dist/{release-command-DN75G5GJ.js → release-command-4EXVLYV2.js} +7 -7
  26. package/dist/{test-command-RRNNJ45I.js → test-command-2F5LKMI6.js} +3 -3
  27. package/dist/{upgrade-command-XFOAWDI3.js → upgrade-command-QL523I62.js} +5 -5
  28. package/dist/{validate-command-GLF5ZOOW.js → validate-command-SK5FPCV7.js} +5 -5
  29. package/dist/{workspace-command-QCFELEGR.js → workspace-command-HYA6JEKK.js} +8 -8
  30. package/guides/authoring.md +9 -1
  31. package/llms/fields.md +3 -2
  32. package/llms/statements-calls.md +2 -1
  33. package/llms/statements-data.md +5 -2
  34. package/llms/tests.md +2 -1
  35. package/llms-full.txt +17 -11
  36. package/llms.txt +5 -5
  37. package/manifest.json +1 -1
  38. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.8
1
+ # xanots v0.0.9
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -45,7 +45,7 @@ installed), so a plain file read resolves them at the version you have.
45
45
  - [Triggers](llms/triggers.md): Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.
46
46
  - [Array and database statements](llms/statements-data.md): Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).
47
47
  - [Statement runtime behavior](llms/statements-runtime.md): 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.
48
- - [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
48
+ - [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
49
49
  - [Value catalog](llms/values.md): Read when you need a literal, a reference, or a tag you have not used before — `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.
50
50
  - [Column and input types](llms/fields.md): 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.
51
51
  - [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
@@ -358,7 +358,7 @@ Non-obvious authoring rules:
358
358
  `s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
359
359
  take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
360
360
  `s.group(body)` and `s.util.post_process(body)` take it **positionally**.
361
- `s.for` is **count-bounded** (`{ as, count, body }`), not from/to. See the
361
+ `s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
362
362
  authored signatures in `llms/statements-data.md`.
363
363
  - **MCP servers & agents are distinct root kinds** that both persist under the
364
364
  `toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes
@@ -474,14 +474,14 @@ Control flow & blocks (each nests a sub-stack; block specials name it `body`):
474
474
  - **Every** statement takes `disabled?`/`description?` — annotations on the stack item, not args: `disabled: true` is Xano's "disable step" (kept in the stack, skipped at runtime), `description` the note beside it. Inline on object-arg factories; a trailing object on the positional ones (`s.set_var("x", v, { disabled: true })`).
475
475
  - **Statements with an `as`** also take `asFilters?` — `fl.*` filters on the RESULT as it binds, in order, same slot as `disabled`: `s.set_var("x", v, { asFilters: [fl.trim(), fl.lower()] })`. Saves a follow-up `set_var`. Throws without an `as`. The bound variable is RETYPED by the chain (`db.query` + `[fl.count()]` → `number`); filters whose result the engine declares as `any` (`get`, `set`, `json_decode`, …) fold to `unknown`.
476
476
  - `s.conditional({ when, then, elif?, else? })` — if/elif/else. `when` is a condition (`expr`/`cmp`/`and`/`or`); `elif` is an ordered `[{ when, then }]` (each an else-if branch); `then`/`else` are `Statement[]`.
477
- - `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to.
477
+ - `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to; `count` is a `Value`, not a bare number.
478
478
  - `s.foreach({ as, list, body })` — iterate `list`; `as` is the current item.
479
479
  - `s.while({ when, body })` — `when` is a condition (`expr`/`cmp`/`and`/`or`).
480
480
  - `s.switch({ on, cases: [{ when, body, break? }], default? })` — multi-way branch on a subject `Value` `on`; each `case`'s `when` is a literal `Value` matched against `on` (NOT a comparison — use `s.conditional` for `<`/`>`/ranges). ⚠ **Omitting `break: true` FALLS THROUGH** — the matched case also runs every LATER case body. Type-checks clean; only `export --strict` catches it.
481
481
  - `s.try_catch({ try, catch?, finally? })` — three `Statement[]` blocks.
482
482
  - `s.group(body)` / `s.util.post_process(body)` — take a `Statement[]` **positionally**.
483
483
  - `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` — nullary loop control.
484
- - `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise.
484
+ - `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise; `exception` is a `Value`, not a bare string.
485
485
 
486
486
  # Object kinds
487
487
 
@@ -630,7 +630,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
630
630
 
631
631
  - 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.
632
632
  - `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.
633
- - `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. `exception` is text the raised message must CONTAIN; omit it to accept any error.
633
+ - `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. `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.
634
634
  - `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.
635
635
  - An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.
636
636
  - Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.
@@ -643,6 +643,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
643
643
  - `xanots test list` shows what is there without running it; `xanots test run "<name>"` runs one. When a name is ambiguous the error prints the qualified `function:math/happy path` form, which `run` also accepts.
644
644
  - `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
645
645
  - A failing suite exits 5, distinct from a crash. Tests that could not be REACHED exit 6 — retry that one, investigate the other. An environment with no tests is success, not failure.
646
+ - For CI, the exit code says THAT something failed and the JSON says WHICH. Every progress line goes to stderr and stdout carries one JSON document — emitted whenever stdout is not a terminal, or on demand with `--json`. `run-all` and `run`: `{ dest, env, total, passed, failed, tests: [{ kind, name, object?, status: "pass"|"fail", message?, timing? }] }`, with the same keys on an empty suite. `list` is `{ dest, env, total, tests: [...] }` and `deploy --test` nests the run under `testRun`. The per-test array is always `tests`.
646
647
  - `xanots deploy ./index.ts --test` deploys and then runs the suite against what it just shipped. A failure exits 5 WITHOUT retracting the deploy — the environment is live either way.
647
648
 
648
649
  # Agent and MCP def shapes
@@ -792,7 +793,8 @@ DB reads/writes (`table` is a def handle or name; `fieldName` defaults to the
792
793
  primary key `id`):
793
794
 
794
795
  - `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` — one row by field match; `output` restricts returned columns (and overrides column visibility — it can pull `internal` columns like a password hash).
795
- - `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` like `db.get`. Both spellings are live in pulled workspaces.
796
+ - `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` for an id that names no row. Both spellings are live in pulled workspaces.
797
+ - ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`.
796
798
  - `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
797
799
  - `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
798
800
  - `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
@@ -800,8 +802,9 @@ primary key `id`):
800
802
  - `null` is accepted on EVERY column, including ones that refuse every other literal, and encodes `const:null` — a write OF null, not the same as omitting the key. A column's `nullable` is not consulted at encode; the engine refuses a null it forbids.
801
803
  - Omitting a key on `add` writes the column's type default — declared `default` if set, `[]` for a list, `{}` for obj/json, else `null`. That `null` is EMITTED, not what the row holds: the engine applies the column's nullability, so a `nullable` column keeps `null` and a non-nullable one lands on its type's zero value (`""`, `0`, `false`). Set the cell when the stored value matters. On `edit` an omitted key keeps its stored value.
802
804
  - An `f.password()` cell takes the PLAINTEXT — the column hashes on write, so a pre-hashed value, or a hashing filter on the cell, stores a hash of a hash that `security.check_password` can never match.
805
+ - A `table({ seed })` cell hashes the same way: the import writes the plaintext through the column's own rules, so a seeded credential matches under `security.check_password` exactly as an added one does. Demo accounts work as fixtures — the usual caution about seed data applies, since the plaintext sits in the repo.
803
806
  - `s.db.edit({ table, fieldName?, fieldValue, row?, data?, output?, as? })` — update by field match.
804
- - `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial (`data` is an object value).
807
+ - `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial. ⚠ Unlike `db.edit`'s `row`, `data` is a single object `Value` — write `obj({ unread: c.int(0) })`, not the column-keyed record `row` takes.
805
808
  On these three, `output` restricts the columns of the RETURNED row only — it does not change
806
809
  what is written. Not offered on `db.del`/`db.has` (their result is a scalar) or on
807
810
  `db.add_or_edit` (no output envelope).
@@ -810,6 +813,7 @@ primary key `id`):
810
813
  - `where` / `additionalWhere` — `expr(...)`, an `expr[]` (ANDed), or a raw `Value`. Rides `context.search`.
811
814
  - ⚠ `ignoreEmpty` DROPS the predicate when the operand is empty — it does not match zero rows. On an `in` comparison an empty list therefore returns the UNFILTERED set, so never use it to scope rows to a permitted-id list: an empty list of permissions returns everything.
812
815
  - For the full operator set use `cmp(left, op, right, { ignoreEmpty? })` — `op`: `in`/`not in`/`like`/`ilike`/`between`/`contains`/`includes`/`overlaps`/`@>`/`~`/`search`/… plus the `expr` comparisons. Database-only — a runtime condition takes the `expr` set only.
816
+ - ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
813
817
  - Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
814
818
  - An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
815
819
  - ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
@@ -890,7 +894,7 @@ Runtime behavior (what the `as:` output holds, and misses):
890
894
 
891
895
  # Auth, cross-object calls, and microservices
892
896
 
893
- > Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
897
+ > Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
894
898
 
895
899
  Auth & calls:
896
900
 
@@ -906,6 +910,7 @@ Auth & calls:
906
910
  - `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.
907
911
  - `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`.
908
912
  - `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.
913
+ - `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.
909
914
  - `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.
910
915
  - `s.task.call` / `s.tool.call` / `s.trigger.call` / `s.middleware.call` / `s.addon.call` — same `{ <target>, input?, as? }` shape against the named kind. `s.task.call` and `s.trigger.call` are WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`); `s.tool.call`, `s.middleware.call` and `s.addon.call` run from any stack.
911
916
  - `s.action.call({ actionId, input?, registry?, as? })` / `s.action.package.call({ traceId, versionId, slug, input?, registry?, as? })` — invoke an installed action. Ids are SUPPLIED, never derived from a name: an action is installed onto the instance, so its identity is assigned at install and differs per instance — read them off a call in a pulled workspace. The package form needs all three parts; they are one composite and two of them address nothing. `registry` is the action's own settings, `input` the per-call arguments.
@@ -986,8 +991,9 @@ an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/
986
991
  `s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather
987
992
  than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for
988
993
  "not set yet" — `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and
989
- binds `null`, which is the answer the null was reaching for. `export()` warns on a literal
990
- `c.null()` in that slot.
994
+ binds `null`, which is the answer the null was reaching for and never `s.db.get_by_id`,
995
+ which validates `id >= 1` and fails the whole request on the sentinel. `export()` warns on a
996
+ literal `c.null()` in that slot.
991
997
  An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through
992
998
  any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:
993
999
  "vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.8
1
+ # xanots v0.0.9
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -45,7 +45,7 @@ installed), so a plain file read resolves them at the version you have.
45
45
  - [Triggers](llms/triggers.md): Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.
46
46
  - [Array and database statements](llms/statements-data.md): Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).
47
47
  - [Statement runtime behavior](llms/statements-runtime.md): 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.
48
- - [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
48
+ - [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
49
49
  - [Value catalog](llms/values.md): Read when you need a literal, a reference, or a tag you have not used before — `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.
50
50
  - [Column and input types](llms/fields.md): 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.
51
51
  - [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
@@ -358,7 +358,7 @@ Non-obvious authoring rules:
358
358
  `s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
359
359
  take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
360
360
  `s.group(body)` and `s.util.post_process(body)` take it **positionally**.
361
- `s.for` is **count-bounded** (`{ as, count, body }`), not from/to. See the
361
+ `s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
362
362
  authored signatures in `llms/statements-data.md`.
363
363
  - **MCP servers & agents are distinct root kinds** that both persist under the
364
364
  `toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes
@@ -474,11 +474,11 @@ Control flow & blocks (each nests a sub-stack; block specials name it `body`):
474
474
  - **Every** statement takes `disabled?`/`description?` — annotations on the stack item, not args: `disabled: true` is Xano's "disable step" (kept in the stack, skipped at runtime), `description` the note beside it. Inline on object-arg factories; a trailing object on the positional ones (`s.set_var("x", v, { disabled: true })`).
475
475
  - **Statements with an `as`** also take `asFilters?` — `fl.*` filters on the RESULT as it binds, in order, same slot as `disabled`: `s.set_var("x", v, { asFilters: [fl.trim(), fl.lower()] })`. Saves a follow-up `set_var`. Throws without an `as`. The bound variable is RETYPED by the chain (`db.query` + `[fl.count()]` → `number`); filters whose result the engine declares as `any` (`get`, `set`, `json_decode`, …) fold to `unknown`.
476
476
  - `s.conditional({ when, then, elif?, else? })` — if/elif/else. `when` is a condition (`expr`/`cmp`/`and`/`or`); `elif` is an ordered `[{ when, then }]` (each an else-if branch); `then`/`else` are `Statement[]`.
477
- - `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to.
477
+ - `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to; `count` is a `Value`, not a bare number.
478
478
  - `s.foreach({ as, list, body })` — iterate `list`; `as` is the current item.
479
479
  - `s.while({ when, body })` — `when` is a condition (`expr`/`cmp`/`and`/`or`).
480
480
  - `s.switch({ on, cases: [{ when, body, break? }], default? })` — multi-way branch on a subject `Value` `on`; each `case`'s `when` is a literal `Value` matched against `on` (NOT a comparison — use `s.conditional` for `<`/`>`/ranges). ⚠ **Omitting `break: true` FALLS THROUGH** — the matched case also runs every LATER case body. Type-checks clean; only `export --strict` catches it.
481
481
  - `s.try_catch({ try, catch?, finally? })` — three `Statement[]` blocks.
482
482
  - `s.group(body)` / `s.util.post_process(body)` — take a `Statement[]` **positionally**.
483
483
  - `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` — nullary loop control.
484
- - `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise.
484
+ - `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise; `exception` is a `Value`, not a bare string.
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xanots",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.",
5
5
  "coverage": {
6
6
  "objectKinds": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xanots/sdk",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "XanoTS — your Xano backend as TypeScript. `xanots deploy` ships your typed workspace (and an optional static frontend) to a live, auto-expiring ephemeral environment and prints its URL. init → deploy → URL.",
5
5
  "keywords": [
6
6
  "xanots",