Skip to main content

Bundle key names vs. the names everything else uses

A bundle’s payload 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.
After a command succeeds the CLI checks npm (at most once an hour, cached in ~/.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:
A registry it cannot reach is an error (exit 1), never a quiet “you are up to date”. Without --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.
Four entry points, and only the first is the authoring API. @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.