Skip to main content
A table is a table({ name, schema }) def object. The schema is a map of column name to an f.* field, and the row type follows from it.

Define tables

  • id and created_at are added at the head of every schema unless you declare them. idType: "uuid" gives a uuid primary key instead of an int.
  • f.tableRef(table) links to another table. The target must be registered on the same workspace, or the export fails and names it.
  • { array: true } turns any scalar into a list column.
  • "unique" in an index is shorthand for "btree|unique".
Register both in xano/index.ts with .registerTables([users, posts]).

Seed rows

Give a table seed rows and they are written when you deploy, so a fresh Xano Engine or ephemeral starts with data:
Rows are type-checked against the schema. Omit id and rows are numbered for you. For a larger set, keep the rows in a file with seed: seedFile("./categories.seed.json", import.meta.url). Seed data is fixture data: keep secrets out of it.

Read and write rows

Endpoints, functions, tasks and triggers reach tables through the s.db.* statements:
What each statement binds decides the endpoint’s response type: s.db.bulk.update replaces every column an item omits with its zero value. Use s.db.bulk.patch to update only the keys each item carries. To return each post with its author, attach an addon to the read. Do not run a second query and loop over the rows to stitch them together. An addon is a single read against one table, bound to the parent row through its inputs:
Attach it with the read’s addon option, and map the parent row’s columns into its inputs with out():
Register the addon with .registerAddons([author]). s.db.get, add, edit and patch take the same addon option. Passing the handle types the graft in the endpoint’s response, so the typed frontend sees _author. An as that shadows a real column fails the build, which is why the alias starts with _.

React to row changes: table triggers

A tableTrigger runs a stack when rows in its table are inserted, updated, deleted or truncated. It fires on every write, whichever endpoint, function or task made it. Put the same side effect in one endpoint’s stack and every other write path skips it. A trigger has no input. Its stack is a callback that receives a handle t: t.new is the row after the change, t.old the row before it, and t.action the operation.
Register them with .registerTriggers([onPostChange, onPublish]).
  • Bind table to the handle. t.new("title") and t.old("title") are then typed to the posts row, and a misspelled column does not compile.
  • The absent side is {}, not null. An insert’s t.old and a delete’s t.new are empty objects, so branch on t.action as above rather than on a null check.
  • search runs before the stack. It is the same expr() surface as a where, with columns addressed as NEW.<column> and OLD.<column>. It cannot be combined with truncate.
  • Triggers return nothing. A table trigger is config only, with no response.
To run one on demand while you develop, ask your coding agent to run it through the Xano Engine’s MCP server.

Use the row type

InferRow gives you a table’s row type anywhere in TypeScript:
Rename or retype a column and every consumer stops compiling. The typed frontend uses the same types.

Changing a table later

npm run xano:deploy merges into the Xano Engine’s existing data, and prints what a schema change will cost (dropped columns, retyped columns) before it writes. Table changes that reach a shared workspace are not rolled back by a later release, so read Schema changes before you drop or retype a column there.

Next