query() def object published under an apiGroup(). Its public path is
/api:<canonical>/<name>, and its stack is the logic that runs per request. Logic that more
than one caller needs lives in a function, and steps that wrap many objects live in middleware.
An API group and its endpoints
xano/index.ts with .registerApiGroups([blog]) and
.registerQueries([getPost, createPost]).
- Path params. Each
{param}segment innameneeds an input of the same name, orquery()throws. Read it withinp()like any other input. Inputs not in the path are query-string params on a GET and body fields otherwise. - Identity. An endpoint is identified by its group, verb and name, so
GET postsandPOST postsare two endpoints. Changing the verb is an identity change, not an edit. - Errors with a status.
s.preconditionfails the request with the status itserror_typenames:badrequest400,unauthorized401,accessdenied403,notfound404,toomanyrequests429.s.throwanswers 200 with an error body, so a client cannot detect it from the status. - Auth.
auth: usersnames an auth table (table({ auth: true })). The caller sends a bearer token for a row in it, andauth("id")reads that row’s id. For signup and login endpoints, install the auth add-on or follow the email and password pattern in the authoring reference. - CORS.
apiGroup({ cors: { mode: "custom", allowOrigins: [...], allowMethods: {...} } })answers browsers on known origins.mode: "default"serves a permissive policy.
Functions
AdefineFunction() is logic other code calls. Only a query is reachable by verb and path, and
only a tool is callable by an agent or an MCP client, so logic that serves an endpoint and a
tool lives in one function both of them run with s.function.run:
.registerFunctions([postsByAuthor]), .registerQueries([authorPosts])
and .registerTools([authorPostsTool]).
inputis keyed by the function’s input names. An undeclared name is a type error. Pass every input explicitly: one you omit is filled from the caller’s own input of the same name, if it has one.- The result is typed.
asbinds the function’s response type, soref("rows")carries the row type into the endpoint’s response. - Functions take tests. Give one
testsand it is asserted on its own. See Tests.
Work a request does not wait for
Addruntime: { mode: "async-shared" } to s.function.run and the call moves off the request
path. The endpoint answers straight away and the function runs on the instance’s async
workers:
as binds a job id, and s.await
collects results by id. That also runs independent work side by side:
- A timeout returns nothing. A run still going when
timeoutpasses, or an unknown id, fails the request with a 500Async timeout. There is no partial result. "async-dedicated"reserves its own resources: it readscpu,memory,timeoutandmaxRetryfrom the sameruntimeblock.s.ai.agent.runtakes the sameruntime.- Work on a timer is a task, and work that follows a row change is a table trigger.
Middleware
Amiddleware() is a reusable stack that runs before (pre) or after (post) its host: an auth
check, a rate limit or logging that many objects share, instead of the same steps repeated at
the top of each stack. You attach it to a query, a function, a task, a tool or an API group, or
to every host of a kind at once with workspaceConfig({ middleware }). This one rate-limits a
public endpoint by client IP:
middleware field, and register it with
.registerMiddleware([publicRateLimit]):
.registerWorkspace(config):
query, function, task and tool keys. Setting it replaces the whole
workspace map, so a host kind you leave out has its workspace middleware cleared. Omit
middleware to leave what the workspace holds untouched.
What to know before you write your own:
exceptionPolicydecides whether a guard guards. The SDK writes"rethrow"by default, which aborts the request."silent"swallows the error and lets the request through, so use it only for advisory middleware.- Inheritance is override. A phase you set on an endpoint replaces the API group’s chain for
that phase, and the group’s replaces the workspace’s.
pre: middleware.clear()sets a phase to nothing. - Key a public endpoint by IP. On an endpoint with no
auth,auth("id")does not resolve and the request fails with a 403. Usesys.remoteIp()there, as above. - One middleware object is one counter. Attaching the same rate limit to several endpoints
shares its key, so
maxbecomes a budget across all of them. Vary the key prefix for a separate limit per endpoint. - Redis is an instance capability.
s.redis.*statements fail at request time on an instance without cache support.
Next
- Webhooks, request history,
resultStrategyand every statement: authoring reference. - Call these endpoints from a frontend with types: Typed frontend.
- Assert they work: Tests.
- Run a stack on a schedule: Tasks.