Skip to main content
An endpoint is a 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

Register them in xano/index.ts with .registerApiGroups([blog]) and .registerQueries([getPost, createPost]).
  • Path params. Each {param} segment in name needs an input of the same name, or query() throws. Read it with inp() 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 posts and POST posts are two endpoints. Changing the verb is an identity change, not an edit.
  • Errors with a status. s.precondition fails the request with the status its error_type names: badrequest 400, unauthorized 401, accessdenied 403, notfound 404, toomanyrequests 429. s.throw answers 200 with an error body, so a client cannot detect it from the status.
  • Auth. auth: users names an auth table (table({ auth: true })). The caller sends a bearer token for a row in it, and auth("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

A defineFunction() 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:
Register them with .registerFunctions([postsByAuthor]), .registerQueries([authorPosts]) and .registerTools([authorPostsTool]).
  • input is 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. as binds the function’s response type, so ref("rows") carries the row type into the endpoint’s response.
  • Functions take tests. Give one tests and it is asserted on its own. See Tests.

Work a request does not wait for

Add runtime: { 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:
An async call does not return the function’s result. Its 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 timeout passes, or an unknown id, fails the request with a 500 Async timeout. There is no partial result.
  • "async-dedicated" reserves its own resources: it reads cpu, memory, timeout and maxRetry from the same runtime block. s.ai.agent.run takes the same runtime.
  • Work on a timer is a task, and work that follows a row change is a table trigger.

Middleware

A middleware() 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:
Attach it with the host’s middleware field, and register it with .registerMiddleware([publicRateLimit]):
To attach it to every endpoint at once, set the workspace tier and register the config with .registerWorkspace(config):
The map takes 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:
  • exceptionPolicy decides 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. Use sys.remoteIp() there, as above.
  • One middleware object is one counter. Attaching the same rate limit to several endpoints shares its key, so max becomes 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