Running it
A pull isinit with xano/ filled from an existing backend instead of the empty starter,
so it is the scaffold command with one extra flag:
--framework, the theme flags, --ai and
--web mean the same thing here as they do without --from. The two flags that describe
the pull itself — --report and --skip-roundtrip — are refused without it rather than
silently ignored.
The generated tree
Inside,xano/ is shaped the way the workspace is: one directory per kind, with each
object under its parent — queries under the API group that owns them, triggers under
what they fire on. Each table gets its own table/<name>.ts, settings sit in xano/workspace.ts,
_shared.ts holds anything else referenced from more than one file, and xano/README.md
lists anything that did not translate cleanly.
Object identities (guid) are preserved, so cross-references stay intact. A statement
this SDK does not model yet round-trips verbatim rather than breaking the pull.
Pulled objects are authored the same way you would write them by hand — table({...}),
query({...}), defineFunction({...}) — so the generated tree keeps its types. A pulled
table’s columns still check on fieldName/output/sortBy, InferInput<typeof q> still
resolves a pulled query’s payload, and a pulled agent still types s.ai.agent.run.
A pull states what the source workspace actually holds and leaves out what the SDK would
put back anyway. A table’s primary(id) / created_at / gin(xdo) indexes are the
engine’s standard set, so only the indexes someone created are listed. A trigger comes back
through the factory that built it (tableTrigger, realtimeTrigger, …) rather than a bare
satisfies TriggerDef, which keeps its typed stack handle; the two realtime types that bind
a def handle are the exception, since a stored trigger carries two guids with no way to know
they agree. And two objects that reference each other — a pair of tables joined both ways,
two functions that call each other — can’t both be declared first, so the second reference
is a {name, guid} const hoisted to the top of the file (const OrdersRef = {…}) instead of
an import that would close a cycle. Only the guid is ever read, so it binds exactly.
xano/README.md also lists objects that were already empty in the source — an
endpoint someone created and never filled in pulls as a def with no stack, which looks
identical to a decode that gave up. The report is what tells the two apart.
A few options exist only so a pull can be faithful, and reading them in generated code
is the only time you should see them: table: null / fn: null (a statement whose target
was deleted or never bound), merge / hidden on a field, paging: { enabled } on a
query, c.blank(tag) (the editor’s unconfigured value box — not a zero or an
empty collection; the engine reads "" and "0" differently, so tidying one into the
other changes what the workspace stores), and c.null("const:obj") (the object-typed null
a db.* statement’s @meta slot carries — different stored bytes from c.obj(null),
which is the blank object, though both evaluate to null). They describe what the source workspace actually
stored — a pulled table: null is a defect to fix upstream, not a shape to copy — and
each carries that warning at the call site. A blank binding also reports, because a
statement wired to a table or function that no longer exists is worth seeing even though
it round-trips exactly.
Verification, and the decode report
Then it checks its own work: the project it just wrote is loaded, exported, and diffed against the workspace it came from. A mismatch names the object and fails the command (--skip-roundtrip opts out). So “it compiled” and “it means the same thing” are separate
claims, and you get both.
The findings above are printed either way. Verification runs after decoding is finished,
so whether it passes, disagrees, or cannot run at all, the report describing the decode is
rendered first — and a tree that was written but does not re-export exits 2, distinct
from the 1 you get when nothing was written because the bundle could not be read.
Reading the report. It opens with a headline (27 distinct issues across 424 findings; 3 need your attention) and splits into three sections, most actionable first: problems in
your workspace, things XanoTS could not model, and the things stated only so the output
is not ambiguous. That split is the question a reader actually has — a raw() passthrough
is ours to close, a lambda reading an unbound name is theirs to fix, and an empty object is
neither. Findings that repeat the same sentence across objects collapse to one line with a
count and a collapsed object list, and each names the generated file it landed in.
--report full prints every site instead; --report json prints the findings as data, and
the same data is written into xano/.xanots-codegen.json on every pull, so parity is
trackable release over release and gateable in CI without scraping output.
Re-pulling is a real workflow: a second init --from into the same directory refreshes
xano/ and leaves the rest of the project — your package.json, your frontend/ —
exactly as you left it. No --force needed, because the tree carries a marker saying it
was machine-written.
⚠️xano/is a scratch surface, and there is noxanots workspace deploy. Regenerating rewrites it (a directory that isn’t a previous pull still needs--force), it carries schema only — no table rows — and deploying it is a full replace of the target. Pull from your real workspace, edit, anddeployto a disposable ephemeral environment. Workspace env var values ride inline inxano/workspace.ts(that is what a deploy sends), so treat a pulled tree as secret-bearing before you commit it.