Bundle key names vs. the names everything else uses
A bundle’spayload arrays are keyed by the engine’s storage name, which for five kinds
is not the name the SDK, the CLI, or a release plan uses for the same thing. Scripting against
a bundle means translating:
Everything else (
query, function, task, trigger, middleware, microservice,
addon, workflow_test, workspace) is keyed the same on both sides.
Two traps worth naming. A release plan reports operations as {"type": "table", …} while the
bundle it came from stores that object under payload.dbo — so a table that released
correctly looks missing if you go looking for payload.table. And agent and mcp_server
both land in payload.toolset, while the kind actually named toolset lands in
payload.tool; matching on the word alone will pick the wrong array.
This is also the format xanots lock import reads, so both directions are user-facing.
Build warnings, and --strict. export/deploy print a xanots: warning for the
shapes that ship clean and then do the wrong thing — a bulk.update zero-filling the columns
an item omits, an ignoreEmpty on an operand that’s already empty, a ref() no as binds,
a filter name the engine can’t resolve, a s.switch case that falls through into the next
one, a request-time timestamp filter on a where operand. Each stays a warning because each
has a legitimate use. Nothing fails on a message nobody reads, though, so pass --strict in CI and in
unattended agent builds: every warning becomes a hard failure and the exit code carries it.
The programmatic equivalents are emitBundle(app, { strict: true }) and
app.export({ strict: true }). The bundle bytes are identical either way.
Shell completion
xanots completion <bash|zsh|fish> prints a completion script covering every command, verb, flag,
and closed value set (--env ephemeral|workspace, --format json|multidoc, --ai claude|codex|cursor|none).
It is generated from the CLI’s own command table, so it never drifts from what the CLI accepts — but it
is baked at generation time, so re-run it after upgrading.
~/.xanots/update-check.json) and prints a one-line nudge to stderr when a newer
@xanots/sdk is published — never to stdout, so piped bundles stay clean. The suggested
command adapts to how you installed it (npm i -g … for a global install, npm i -D …
when it’s a project dependency). The check is best-effort and bounded (a slow or offline
registry never delays a command), and stays silent under CI or when stderr isn’t a terminal.
Opt out with XANOTS_NO_UPDATE_CHECK=1 (or the conventional NO_UPDATE_NOTIFIER=1).
xanots upgrade is the same question asked on purpose, and it answers under all the
conditions the nudge stays quiet for — CI, a piped stderr, the opt-out variables — reading the
registry live rather than serving the hourly cache. --check reports without installing and
exits 7 when a newer version is published, 0 when you are current, so a pipeline can
branch on it:
--check it installs, matching how this CLI is installed — and for a project-local install it
then restores the @xanots/sdk range your project was scaffolded with (npm rewrites it to a
caret, which on the 0.0.x line pins you exactly) and restamps the managed block in your
agent files so the guidance matches the version you now have. Set XANOTS_INSTALL_MODE to
global or local to override the detection.
When something fails
Failures are written for the person reading the terminal. A request the instance refused prints the server’s own sentence on one line (deploy failed (403 Forbidden): Access denied for this workspace.) rather than the whole JSON envelope; a request that never arrived names
what it could not reach and why (workspace list could not reach https://…: fetch failed (ECONNRESET)), and a request that timed out says so separately, because “slow” and
“unreachable” call for different next steps. Set XANOTS_DEBUG=1 to append the untouched
response body underneath — nothing is discarded, only folded away.
A malformed invocation fails before any work happens: a missing or misspelled entry file, an
argument the command has nowhere to put (xanots export --lock xano.lock — that flag takes
its value attached, as --lock=xano.lock), or a missing credential, each answered with the
command’s usage block instead of a stack trace. deploy and release check that you are
signed in before compiling, so a lapsed session costs you a message, not a build.
Emitters that write to disk (and the programmatic CLI) are Node-only — import them from
@xanots/sdk/node. The string emitters (emit, emitBundle) stay on the browser-safe
@xanots/sdk entry.
@xanots/sdk is what you
define a workspace with; @xanots/sdk/node adds the filesystem half; @xanots/sdk/codegen
is what a generated tree imports; and @xanots/sdk/internal holds the compiler machinery —
the per-kind encoders, the kind and statement registries, the bundle serializer, the
xano.lock model. Nothing on /internal is needed to author anything, and it is kept off
the root so an agent scanning the package’s exports sees the surface rather than the guts.
All three run the same build-time checks, including seed validation of a literal
seed: [...] array. A deferred seed (a thunk, or seedFile()) needs an await or the
filesystem, so it is materialised and checked only by xanots export / xanots deploy.