Definitive guide for writing native XanoScript that works on the first deploy. Closed sets, gotchas, and patterns.
/api:GROUP/endpoint/api:GROUP/items/api:GROUP/items/123/api:GROUP/items/api:GROUP/items/123/api:GROUP/items/123$ npx snappy-skills install xanoscript-builder
Write native XanoScript that works on the first deploy. Every concept uses closed sets (exhaustive valid values) so you never guess.
Rule: If it's not documented here as valid, it doesn't exist.
Workflow: Write raw XanoScript using this guide → Deploy to Xano → Curl test → Iterate.
XanoScript is a constrained DSL, not a programming language. Three rules:
db.query is different from what works inside varConsequence: Never assume syntax from JavaScript/Python will work. Always check the closed set first.
xanoscriptquery "path/name" verb=POST {
api_group = "Group Name" // optional
auth = "users" // optional - table name for JWT auth (use EXACT table name including emoji prefix if any!)
input {
text email filters=trim|to_lower
text password
int page?=1 // ? = optional, ?=1 = default
}
stack {
// All logic goes here
}
response = { success: true } // or response = $variable
tags = ["tag1", "tag2"] // optional
}
Endpoint valid blocks: api_group, auth, input, stack, response, tags - NOTHING ELSE.
Valid verbs: GET, POST, PATCH, PUT, DELETE - NOTHING ELSE.
input {} is REQUIRED even if empty - omitting it throws "Missing input block" at deploy.
xanoscriptfunction "Folder/function_name" {
input {
int user_id
text message?
}
stack {
// Logic here
}
response = { success: true, data: $result }
}
Function valid blocks: input, stack, response - NOTHING ELSE.
xanoscripttask "task_name" {
stack {
// No input, no response - runs on schedule
}
}
Task valid blocks: stack - NOTHING ELSE. Tasks have NO input and NO response.
xanoscriptinput {
text name filters=trim // required text
int amount // required integer
bool active? // optional boolean (? suffix)
text status?=pending // optional with default (NO SPACES in defaults!)
email user_email filters=trim // email type with filter
json metadata? // optional JSON
decimal price? // optional decimal
}
Valid input types: text, int, bool, email, password, decimal, json, timestamp, date, enum, uuid, file, image, video, audio - see closed-sets.md.
xanoscriptvar $name { value = "expression" } // Declare
var.update $name { value = "new_value" } // Update (NEVER re-declare!)
Special variables (no $ prefix): now (current timestamp)
Special variables ($ prefix): $auth.id (after auth), $input.*, $db.table.field (in WHERE), $env.KEY
$env.KEY behavior: Missing env vars return null (no error thrown). See loose equality below.
System env vars NOT available in raw XanoScript (runtime-verified):
$remote_ip, $http_headers, $request_method, $request_uri, $request_querystring, $datasource, $branch exist in Xano's visual builder dropdown but are NOT accessible in raw XanoScript via any tested mechanism ($remote_ip, $env.remote_ip, util.get_env). These are visual-builder-only features.
XanoScript has loose equality: null == "", null == 0, null == false, 0 == false, false == "", [] == null are ALL true. Only 0 == "" is false.
Consequence: $env.MISSING_KEY returns null, and null == "" is true. You cannot distinguish missing from empty with ==.
xanoscript// WRONG: Re-declare to update
var $count { value = $count + 1 }
// RIGHT: Use var.update
var.update $count { value = $count + 1 }
This is where most mistakes happen. Read carefully.
CRITICAL RULE: ALWAYS verify table names and field names in the Xano dashboard (or via API) BEFORE writing any database operation. Wrong names deploy fine but fail at runtime with cryptic errors.
db.query accepts 7 valid blocks: where, sort, return, output, join, eval, addon. Nothing else.
xanoscript// WRONG - "Invalid block: filters"
db.query users { filters = {status: "active"} }
// WRONG - "Invalid block: paging"
db.query users { paging = {page: 1, per_page: 10} }
// RIGHT - valid blocks only
db.query users {
where = $db.users.status == "active"
sort = {created_at: "desc"}
return = {type: "list", paging: {page: 1, per_page: 20, totals: true}}
} as $users
// WITH JOIN + EVAL
db.query orders {
join = {
c: {
table: "customers"
where: $db.orders.customer_id == $db.c.id
}
}
where = $db.orders.customer_id != 0
sort = {created_at: "desc"}
return = {type: "list", paging: {page: 1, per_page: 20}}
eval = {
customer_name: $db.c.name
}
output = ["items.id", "items.total", "items.customer_name"]
} as $result
Join rules: join connects tables, eval pulls joined fields into response. Without eval, joined data doesn't appear. Join alias (e.g. c) is referenced as $db.c.field. See database-operations.md.
Addon rules: Addons enrich each row with related data from pre-defined reusable queries. Use $output.field for per-row field references (NOT $db. or quoted strings).
xanoscriptdb.query products {
return = {type: "list", paging: {page: 1, per_page: 10}}
addon = [{
name: "Category Info"
input: {category_id: $output.category_id}
as: "items.category"
}]
output = ["items.id", "items.name", "items.category"]
} as $result
totals: true in paging: Without it, $result.itemsTotal and $result.pageTotal DON'T EXIST. You only get itemsReceived, curPage, nextPage, prevPage, offset, perPage. Add totals: true when you need total count. See database-operations.md.
xanoscript// VALID operators: == != > < >= <= && || ~(contains)
where = $db.table.field == "value"
where = $db.table.field != null
where = $db.table.status == "active" && $db.table.role == "admin"
where = $db.table.role == "admin" || $db.table.role == "mod"
// WRONG - No ternary in WHERE
where = ($input.status != null) ? ($db.table.status == $input.status) : true
// RIGHT - Use conditional branches for optional filters
conditional {
if ($input.status != null) {
db.query items { where = $db.items.status == $input.status ... } as $items
} else {
db.query items { ... } as $items // no where clause
}
}
xanoscriptreturn = {type: "list"} // all records
return = {type: "list", paging: {page: 1, per_page: 20}} // paginated (no totals)
return = {type: "list", paging: {page: 1, per_page: 20, totals: true}} // with totals
return = {type: "count"} // count only (returns integer)
return = {type: "single"} // first matching record (object)
return = {type: "exists"} // boolean true/false
paging is INSIDE return, never standalone. sort direction MUST be quoted.
{type: "first"} is INVALID - use {type: "single"} instead.
xanoscriptdb.get "table" { field_name = "id", field_value = $id } as $record
db.add "table" { data = { field: $value } } as $new
db.edit "table" { field_name = "id", field_value = $id, data = { field: $value } } as $updated
db.del "table" { field_name = "id", field_value = $id } // db.del NOT db.delete!
Both db.add and db.edit use data = (NOT content =!)
xanoscript// WRONG - "Invalid kind for data - assign:var"
var $update { value = {}|set:"name":$input.name }
db.edit "items" { field_value = $id, data = $update }
// RIGHT - Inline data object with variable references
db.edit "items" { field_value = $id, data = { name: $input.name, updated_at: now } }
See database-operations.md for full reference.
xanoscriptprecondition ($user != null) {
error_type = "notfound"
error = "User not found"
}
| error_type | HTTP Code | Response Code | Use For |
|---|---|---|---|
"standard" |
500 | ERROR_FATAL |
Generic errors (same as omitting error_type) |
"inputerror" |
400 | ERROR_CODE_INPUT_ERROR |
Input validation failures |
"badrequest" |
400 | ERROR_CODE_BAD_REQUEST |
Malformed requests |
"unauthorized" |
401 | ERROR_CODE_UNAUTHORIZED |
Authentication failures (bad/missing token) |
"accessdenied" |
403 | ERROR_CODE_ACCESS_DENIED |
Permission/ownership failures |
"notfound" |
404 | ERROR_CODE_NOT_FOUND |
Record doesn't exist |
"toomanyrequests" |
429 | ERROR_CODE_TOO_MANY_REQUESTS |
Rate limiting |
CRITICAL: Values are validated at RUNTIME, not deploy time. Bad values deploy fine but explode on first request.
ANY other string (like "invalid", "error", "not_found", "field_error") throws a runtime validation error.
xanoscript// WRONG - "invalid" is NOT a valid error_type (common mistake!)
precondition ($input.name != "") { error_type = "invalid", error = "Name required" }
// WRONG - "not_found" has no underscore
precondition ($item != null) { error_type = "not_found", error = "Not found" }
// RIGHT - use "inputerror" for validation
precondition ($input.name != "") { error_type = "inputerror", error = "Name required" }
// RIGHT - simplified (defaults to standard/500)
precondition ($input.name != "") { error = "Name required" }
Security best practice: Use same generic error for user-not-found AND wrong-password:
xanoscriptprecondition ($user != null) { error_type = "accessdenied", error = "Invalid Credentials." }
precondition ($pass_result) { error_type = "accessdenied", error = "Invalid Credentials." }
CRITICAL: elseif and else MUST be on their OWN LINE after the closing }. Parentheses work for all conditions. No backticks needed.
xanoscript// CONDITIONAL with else/elseif - OWN LINE, PARENTHESES (runtime-verified)
conditional {
if ($status == "active") {
// logic
}
elseif ($status == "pending") {
// logic
}
else {
// default
}
}
// WRONG - elseif on SAME LINE as } → "Syntax error: unexpected '{'"
// conditional { if (...) { } elseif (...) { } }
// SWITCH/CASE - BROKEN at runtime (deploys fine but NEVER matches cases!)
// Always use conditional if/elseif instead. See what-doesnt-work.md #24c.
// FOR LOOP - count-based iteration, 0-indexed (runtime-verified)
for (5) {
each as $i {
var.update $results { value = $results|push:$i } // $i = 0, 1, 2, 3, 4
}
}
// WHILE LOOP - condition-based iteration (runtime-verified)
while ($counter < 10) {
each { // "each" block required, NO alias
var.update $counter { value = $counter|add:1 }
}
}
// BREAK and CONTINUE - work inside for/while/foreach (runtime-verified)
for (10) {
each as $i {
conditional {
if ($i == 5) { break } // Exit loop entirely
if ($i == 3) { continue } // Skip this iteration
}
}
}
// FOREACH - NEVER use "item" as alias (shadows internal variable!)
foreach ($users) {
each as $row { // Use $row, $record, $entry - NOT $item
var $name { value = $row.name }
}
}
// TRY/CATCH - DOES NOT WORK in raw XanoScript ("unexpected 'try'")
// Use null-as-success pattern instead:
var $result { value = null }
// ... do risky operation, check result ...
conditional {
if ($result == null) { var.update $result { value = {}|set:"success":true } }
}
// IF-ONLY (no else) - parentheses OK here
conditional {
if ($input.name != "") { var.update $count { value = $count|add:1 } }
}
xanoscript// RIGHT - $row.field access works fine in foreach
var $ids { value = [] }
foreach ($query_result.items) {
each as $row { var.update $ids { value = $ids|push:$row.id } }
}
// WRONG - |map:"field" BROKEN → "id is not defined" at runtime
var $ids { value = $query_result.items|map:"id" }
Filters transform values. Chain with |: $value|filter1:param|filter2:param
xanoscript// WRONG - "Invalid filter name: count == 0" (parser reads "count == 0" as one filter)
conditional { if ($arr|count == 0) { } }
// RIGHT - Store filter result in variable, then compare
var $arr_len { value = $arr|count }
conditional { if ($arr_len == 0) { } }
| Need | Filter | WRONG name | ||
|---|---|---|---|---|
| To string | `\ | to_text` | ~~\ | to_string~~ |
| To lowercase | `\ | to_lower` | ~~\ | lowercase~~ |
| To uppercase | `\ | to_upper` | ||
| Trim whitespace | `\ | trim` | ||
| Replace | `\ | replace:"old":"new"` | ||
| Count items | `\ | count` | ~~\ | length~~ |
| First item | `\ | first` | ||
| Push to array | `\ | push:$val` | ||
| Concatenate | `\ | concat:$str` | ||
| Set property | `\ | set:"key":$val` | ||
| Get property | `\ | get:"key":default` | ||
| Index by field | `\ | index_by:"field"` | ~~\ | group_by~~ (note: values are arrays) |
| Add number | `\ | add:N` | ||
| Round | `\ | round:2` | ||
| Time math | `\ | add_secs_to_timestamp:86400` | ||
| Format time | `\ | format_timestamp:"Y-m-d"` | ~~\ | transform_timestamp~~ (returns "0"!) |
| SHA256 | `\ | sha256` | ||
| JSON encode | `\ | json_encode` | ||
| Extract field | ~~\ | map:"field"~~ | BROKEN - use foreach + \ | push |
These look like they should work but FAIL in XanoScript:
| Wrong | Error | Use Instead | |||
|---|---|---|---|---|---|
| `\ | map:"field"` | "field is not defined" at runtime | foreach + `\ | push:$row.field` | |
| `\ | filter:'expr'` | "$item is not defined" | foreach + conditional | ||
| `\ | reduce:'expr':0` | "$item is not defined" | foreach with accumulator | ||
| `\ | to_string` | Invalid filter name | `\ | to_text` | |
| `\ | length` | Invalid filter name | `\ | count (arrays), \ |
strlen` (strings) |
| `\ | group_by:"f"` | Does not exist | `\ | index_by:"f"` (values are arrays!) | |
| `\ | default:val` | Does not exist | conditional fallback | ||
| `\ | contains:val` on arrays | Wrong result (string-only!) | foreach + conditional | ||
| `\ | transform_timestamp` | Returns "0" | `\ | format_timestamp` | |
| `\ | delete:"key"` | Invalid filter name | `\ | unset:"key"` | |
| `\ | implode:sep` | Invalid filter name | `\ | join:sep` |
See filters-reference.md for complete verified list + all non-existent filters.
Chain for performance (600% faster than multiple var.update):
xanoscriptvar $obj { value = {}|set:"a":$x|set:"b":$y|set:"c":$z }
See filters-reference.md for complete list.
xanoscriptvar $auth_header { value = "Authorization: Bearer "|concat:$env.API_KEY }
api.request {
url = "https://api.example.com/endpoint"
method = "POST" // Can also be a variable: method = $input.http_method
params = $request_body // PARAMS not body!
headers = []|push:$auth_header|push:"Content-Type: application/json"
timeout = 120
} as $result
// Response is NESTED - check timeout FIRST
conditional {
if ($result.response.result == false) {
throw { name = "TIMEOUT" value = "API call timed out" }
}
}
var $data { value = $result.response.result }
var $status { value = $result.response.status }
Input defaults gotcha: URLs and strings with special chars (://, ?, &) CANNOT be used as input defaults (text url?=https://... causes error 524). Make such inputs required instead.
xanoscriptfunction.run "Folder/FunctionName" {
input = {
param1: $value1
param2: "literal"
}
} as $result
xanoscriptsecurity.check_password {
text_password = $input.password
hash_password = $user.password
} as $pass_result
// CRITICAL: Uses `table =` NOT `auth =`. ALL 4 blocks REQUIRED.
security.create_auth_token {
table = "🔐 [AUTH] users" // Exact table name (including emoji if any!)
id = $user.id
extras = {} // Custom JWT claims: {}|set:"role":"admin"
expiration = 604800 // seconds (604800 = 7 days)
} as $authToken
See security-methods.md for full auth patterns.
xanoscript// Get ALL user-defined env vars as one object (does NOT include system env vars)
util.get_env {} as $all_env
var $api_key { value = $all_env|get:"openai_secret":"" }
// Get ALL raw input including undeclared query params
util.get_input {} as $raw_input // {test_param: "hello", foo: "bar"}
// Get only declared inputs (matching input {} block)
util.get_all_input {} as $declared // {test_param: "hello"}
// Get all in-scope variables
util.get_vars {} as $all_vars
// Set response headers
util.set_header { name = "X-Custom", value = "test" }
// Pause execution (value in milliseconds)
util.sleep { value = 2000 }
// IP geolocation
util.ip_lookup { ip = "8.8.8.8" } as $geo
// Distance between coordinates
util.geo_distance { lat1 = 40.7, lon1 = -74.0, lat2 = 34.0, lon2 = -118.2 } as $dist
util.get_input vs util.get_all_input: get_input returns ALL query params/body data (even undeclared ones). get_all_input returns ONLY params matching the input {} block.
1. Write MINIMAL XanoScript (one operation)
2. Deploy to Xano
3. Curl test IMMEDIATELY
4. Add ONE feature
5. Redeploy (NEVER create duplicates — update the existing endpoint!)
6. Curl test again
7. REPEAT until complete
NEVER build complete solution first. NEVER skip curl testing.
See workflow.md and curl-testing.md.
| Need to... | Read this |
|---|---|
| Copy-paste complete patterns (CRUD, auth, AI, webhooks) | recipes.md |
| Check ALL valid values for any field | closed-sets.md |
| See what DOESN'T work | what-doesnt-work.md |
| Full database operation reference | database-operations.md |
| Auth, password, token patterns | security-methods.md |
| All filter names and gotchas | filters-reference.md |
| Build-test-deploy process | workflow.md |
| Curl testing patterns | curl-testing.md |
Status: PRODUCTION-READY | Design: Closed-set architecture - never guess, always verify
Source: Runtime-verified from extensive stress testing across production workspaces
---
name: xanoscript-builder
category: API
description: >
Definitive guide for writing native XanoScript that works on the first deploy. Covers endpoint creation, function building, db.query WHERE syntax, precondition error_types, filters, api.request, security methods, variable patterns, control flow, and the incremental build-test-deploy workflow. Built from production experience -- every gotcha is runtime-verified.
---
# XanoScript Builder
Write native XanoScript that works on the first deploy. Every concept uses **closed sets** (exhaustive valid values) so you never guess.
**Rule: If it's not documented here as valid, it doesn't exist.**
**Workflow:** Write raw XanoScript using this guide → Deploy to Xano → Curl test → Iterate.
---
## Layer 1: Mental Model
XanoScript is a **constrained DSL**, not a programming language. Three rules:
1. **Closed sets everywhere** - Every block, keyword, and parameter has a finite list of valid values
2. **No freestyle** - Ternary operators, complex expressions, and dynamic constructs fail silently or throw cryptic errors
3. **Context-specific** - What works inside `db.query` is different from what works inside `var`
**Consequence:** Never assume syntax from JavaScript/Python will work. Always check the closed set first.
---
## Layer 2: Containers (endpoint / function / task)
### Endpoint
```xanoscript
query "path/name" verb=POST {
api_group = "Group Name" // optional
auth = "users" // optional - table name for JWT auth (use EXACT table name including emoji prefix if any!)
input {
text email filters=trim|to_lower
text password
int page?=1 // ? = optional, ?=1 = default
}
stack {
// All logic goes here
}
response = { success: true } // or response = $variable
tags = ["tag1", "tag2"] // optional
}
```
**Endpoint valid blocks:** `api_group`, `auth`, `input`, `stack`, `response`, `tags` - NOTHING ELSE.
**Valid verbs:** `GET`, `POST`, `PATCH`, `PUT`, `DELETE` - NOTHING ELSE.
**`input {}` is REQUIRED** even if empty - omitting it throws "Missing input block" at deploy.
### Function
```xanoscript
function "Folder/function_name" {
input {
int user_id
text message?
}
stack {
// Logic here
}
response = { success: true, data: $result }
}
```
**Function valid blocks:** `input`, `stack`, `response` - NOTHING ELSE.
### Task (Background Job)
```xanoscript
task "task_name" {
stack {
// No input, no response - runs on schedule
}
}
```
**Task valid blocks:** `stack` - NOTHING ELSE. Tasks have NO input and NO response.
---
## Layer 3: Inputs & Variables
### Input Types
```xanoscript
input {
text name filters=trim // required text
int amount // required integer
bool active? // optional boolean (? suffix)
text status?=pending // optional with default (NO SPACES in defaults!)
email user_email filters=trim // email type with filter
json metadata? // optional JSON
decimal price? // optional decimal
}
```
**Valid input types:** `text`, `int`, `bool`, `email`, `password`, `decimal`, `json`, `timestamp`, `date`, `enum`, `uuid`, `file`, `image`, `video`, `audio` - see [closed-sets.md](resources/closed-sets.md).
### Variables
```xanoscript
var $name { value = "expression" } // Declare
var.update $name { value = "new_value" } // Update (NEVER re-declare!)
```
**Special variables (no $ prefix):** `now` (current timestamp)
**Special variables ($ prefix):** `$auth.id` (after auth), `$input.*`, `$db.table.field` (in WHERE), `$env.KEY`
**`$env.KEY` behavior:** Missing env vars return `null` (no error thrown). See loose equality below.
**System env vars NOT available in raw XanoScript (runtime-verified):**
`$remote_ip`, `$http_headers`, `$request_method`, `$request_uri`, `$request_querystring`, `$datasource`, `$branch` exist in Xano's visual builder dropdown but are NOT accessible in raw XanoScript via any tested mechanism (`$remote_ip`, `$env.remote_ip`, `util.get_env`). These are visual-builder-only features.
### Loose Equality (runtime-verified)
XanoScript has loose equality: `null == ""`, `null == 0`, `null == false`, `0 == false`, `false == ""`, `[] == null` are ALL `true`. Only `0 == ""` is `false`.
**Consequence:** `$env.MISSING_KEY` returns null, and `null == ""` is true. You cannot distinguish missing from empty with `==`.
```xanoscript
// WRONG: Re-declare to update
var $count { value = $count + 1 }
// RIGHT: Use var.update
var.update $count { value = $count + 1 }
```
---
## Layer 4: Database Operations
**This is where most mistakes happen. Read carefully.**
**CRITICAL RULE: ALWAYS verify table names and field names in the Xano dashboard (or via API) BEFORE writing any database operation. Wrong names deploy fine but fail at runtime with cryptic errors.**
### db.query - The Valid Blocks
db.query accepts **7 valid blocks**: `where`, `sort`, `return`, `output`, `join`, `eval`, `addon`. Nothing else.
```xanoscript
// WRONG - "Invalid block: filters"
db.query users { filters = {status: "active"} }
// WRONG - "Invalid block: paging"
db.query users { paging = {page: 1, per_page: 10} }
// RIGHT - valid blocks only
db.query users {
where = $db.users.status == "active"
sort = {created_at: "desc"}
return = {type: "list", paging: {page: 1, per_page: 20, totals: true}}
} as $users
// WITH JOIN + EVAL
db.query orders {
join = {
c: {
table: "customers"
where: $db.orders.customer_id == $db.c.id
}
}
where = $db.orders.customer_id != 0
sort = {created_at: "desc"}
return = {type: "list", paging: {page: 1, per_page: 20}}
eval = {
customer_name: $db.c.name
}
output = ["items.id", "items.total", "items.customer_name"]
} as $result
```
**Join rules:** `join` connects tables, `eval` pulls joined fields into response. Without `eval`, joined data doesn't appear. Join alias (e.g. `c`) is referenced as `$db.c.field`. See [database-operations.md](resources/database-operations.md).
**Addon rules:** Addons enrich each row with related data from pre-defined reusable queries. Use `$output.field` for per-row field references (NOT `$db.` or quoted strings).
```xanoscript
db.query products {
return = {type: "list", paging: {page: 1, per_page: 10}}
addon = [{
name: "Category Info"
input: {category_id: $output.category_id}
as: "items.category"
}]
output = ["items.id", "items.name", "items.category"]
} as $result
```
See [database-operations.md](resources/database-operations.md).
**`totals: true` in paging:** Without it, `$result.itemsTotal` and `$result.pageTotal` DON'T EXIST. You only get `itemsReceived`, `curPage`, `nextPage`, `prevPage`, `offset`, `perPage`. Add `totals: true` when you need total count. See [database-operations.md](resources/database-operations.md).
### WHERE Syntax - The Closed Set
```xanoscript
// VALID operators: == != > < >= <= && || ~(contains)
where = $db.table.field == "value"
where = $db.table.field != null
where = $db.table.status == "active" && $db.table.role == "admin"
where = $db.table.role == "admin" || $db.table.role == "mod"
// WRONG - No ternary in WHERE
where = ($input.status != null) ? ($db.table.status == $input.status) : true
// RIGHT - Use conditional branches for optional filters
conditional {
if ($input.status != null) {
db.query items { where = $db.items.status == $input.status ... } as $items
} else {
db.query items { ... } as $items // no where clause
}
}
```
### return + paging - The ONLY Valid Syntax
```xanoscript
return = {type: "list"} // all records
return = {type: "list", paging: {page: 1, per_page: 20}} // paginated (no totals)
return = {type: "list", paging: {page: 1, per_page: 20, totals: true}} // with totals
return = {type: "count"} // count only (returns integer)
return = {type: "single"} // first matching record (object)
return = {type: "exists"} // boolean true/false
```
**paging is INSIDE return, never standalone. sort direction MUST be quoted.**
**`{type: "first"}` is INVALID - use `{type: "single"}` instead.**
### CRUD Quick Reference
```xanoscript
db.get "table" { field_name = "id", field_value = $id } as $record
db.add "table" { data = { field: $value } } as $new
db.edit "table" { field_name = "id", field_value = $id, data = { field: $value } } as $updated
db.del "table" { field_name = "id", field_value = $id } // db.del NOT db.delete!
```
**Both db.add and db.edit use `data =` (NOT `content =`!)**
### db.edit - data MUST Be Inline (runtime-verified)
```xanoscript
// WRONG - "Invalid kind for data - assign:var"
var $update { value = {}|set:"name":$input.name }
db.edit "items" { field_value = $id, data = $update }
// RIGHT - Inline data object with variable references
db.edit "items" { field_value = $id, data = { name: $input.name, updated_at: now } }
```
See [database-operations.md](resources/database-operations.md) for full reference.
---
## Layer 5: Precondition & Validation
```xanoscript
precondition ($user != null) {
error_type = "notfound"
error = "User not found"
}
```
### error_type - The Closed Set (EXACTLY 7 values, runtime-verified 2026-02-20)
| error_type | HTTP Code | Response Code | Use For |
|---|---|---|---|
| `"standard"` | 500 | `ERROR_FATAL` | Generic errors (same as omitting error_type) |
| `"inputerror"` | 400 | `ERROR_CODE_INPUT_ERROR` | Input validation failures |
| `"badrequest"` | 400 | `ERROR_CODE_BAD_REQUEST` | Malformed requests |
| `"unauthorized"` | 401 | `ERROR_CODE_UNAUTHORIZED` | Authentication failures (bad/missing token) |
| `"accessdenied"` | 403 | `ERROR_CODE_ACCESS_DENIED` | Permission/ownership failures |
| `"notfound"` | 404 | `ERROR_CODE_NOT_FOUND` | Record doesn't exist |
| `"toomanyrequests"` | 429 | `ERROR_CODE_TOO_MANY_REQUESTS` | Rate limiting |
**CRITICAL:** Values are validated at RUNTIME, not deploy time. Bad values deploy fine but explode on first request.
**ANY other string (like "invalid", "error", "not_found", "field_error") throws a runtime validation error.**
```xanoscript
// WRONG - "invalid" is NOT a valid error_type (common mistake!)
precondition ($input.name != "") { error_type = "invalid", error = "Name required" }
// WRONG - "not_found" has no underscore
precondition ($item != null) { error_type = "not_found", error = "Not found" }
// RIGHT - use "inputerror" for validation
precondition ($input.name != "") { error_type = "inputerror", error = "Name required" }
// RIGHT - simplified (defaults to standard/500)
precondition ($input.name != "") { error = "Name required" }
```
**Security best practice:** Use same generic error for user-not-found AND wrong-password:
```xanoscript
precondition ($user != null) { error_type = "accessdenied", error = "Invalid Credentials." }
precondition ($pass_result) { error_type = "accessdenied", error = "Invalid Credentials." }
```
---
## Layer 6: Control Flow
**CRITICAL:** `elseif` and `else` MUST be on their OWN LINE after the closing `}`. Parentheses work for all conditions. No backticks needed.
```xanoscript
// CONDITIONAL with else/elseif - OWN LINE, PARENTHESES (runtime-verified)
conditional {
if ($status == "active") {
// logic
}
elseif ($status == "pending") {
// logic
}
else {
// default
}
}
// WRONG - elseif on SAME LINE as } → "Syntax error: unexpected '{'"
// conditional { if (...) { } elseif (...) { } }
// SWITCH/CASE - BROKEN at runtime (deploys fine but NEVER matches cases!)
// Always use conditional if/elseif instead. See what-doesnt-work.md #24c.
// FOR LOOP - count-based iteration, 0-indexed (runtime-verified)
for (5) {
each as $i {
var.update $results { value = $results|push:$i } // $i = 0, 1, 2, 3, 4
}
}
// WHILE LOOP - condition-based iteration (runtime-verified)
while ($counter < 10) {
each { // "each" block required, NO alias
var.update $counter { value = $counter|add:1 }
}
}
// BREAK and CONTINUE - work inside for/while/foreach (runtime-verified)
for (10) {
each as $i {
conditional {
if ($i == 5) { break } // Exit loop entirely
if ($i == 3) { continue } // Skip this iteration
}
}
}
// FOREACH - NEVER use "item" as alias (shadows internal variable!)
foreach ($users) {
each as $row { // Use $row, $record, $entry - NOT $item
var $name { value = $row.name }
}
}
// TRY/CATCH - DOES NOT WORK in raw XanoScript ("unexpected 'try'")
// Use null-as-success pattern instead:
var $result { value = null }
// ... do risky operation, check result ...
conditional {
if ($result == null) { var.update $result { value = {}|set:"success":true } }
}
// IF-ONLY (no else) - parentheses OK here
conditional {
if ($input.name != "") { var.update $count { value = $count|add:1 } }
}
```
### foreach Property Access WORKS, |map BROKEN (runtime-verified)
```xanoscript
// RIGHT - $row.field access works fine in foreach
var $ids { value = [] }
foreach ($query_result.items) {
each as $row { var.update $ids { value = $ids|push:$row.id } }
}
// WRONG - |map:"field" BROKEN → "id is not defined" at runtime
var $ids { value = $query_result.items|map:"id" }
```
---
## Layer 7: Filters
Filters transform values. Chain with `|`: `$value|filter1:param|filter2:param`
### Filter Results in Conditionals - MUST Store First
```xanoscript
// WRONG - "Invalid filter name: count == 0" (parser reads "count == 0" as one filter)
conditional { if ($arr|count == 0) { } }
// RIGHT - Store filter result in variable, then compare
var $arr_len { value = $arr|count }
conditional { if ($arr_len == 0) { } }
```
### Key Filters (Correct Names)
| Need | Filter | WRONG name |
|------|--------|------------|
| To string | `\|to_text` | ~~\|to_string~~ |
| To lowercase | `\|to_lower` | ~~\|lowercase~~ |
| To uppercase | `\|to_upper` | |
| Trim whitespace | `\|trim` | |
| Replace | `\|replace:"old":"new"` | |
| Count items | `\|count` | ~~\|length~~ |
| First item | `\|first` | |
| Push to array | `\|push:$val` | |
| Concatenate | `\|concat:$str` | ~~+ operator~~ |
| Set property | `\|set:"key":$val` | |
| Get property | `\|get:"key":default` | |
| Index by field | `\|index_by:"field"` | ~~\|group_by~~ (note: values are arrays) |
| Add number | `\|add:N` | |
| Round | `\|round:2` | |
| Time math | `\|add_secs_to_timestamp:86400` | ~~now + 86400~~ |
| Format time | `\|format_timestamp:"Y-m-d"` | ~~\|transform_timestamp~~ (returns "0"!) |
| SHA256 | `\|sha256` | |
| JSON encode | `\|json_encode` | |
| Extract field | ~~\|map:"field"~~ | **BROKEN** - use foreach + \|push |
### Filters That DON'T Work (Top Mistakes)
These look like they should work but **FAIL in XanoScript**:
| Wrong | Error | Use Instead |
|---|---|---|
| `\|map:"field"` | "field is not defined" at runtime | foreach + `\|push:$row.field` |
| `\|filter:'expr'` | "$item is not defined" | foreach + conditional |
| `\|reduce:'expr':0` | "$item is not defined" | foreach with accumulator |
| `\|to_string` | Invalid filter name | `\|to_text` |
| `\|length` | Invalid filter name | `\|count` (arrays), `\|strlen` (strings) |
| `\|group_by:"f"` | Does not exist | `\|index_by:"f"` (values are arrays!) |
| `\|default:val` | Does not exist | conditional fallback |
| `\|contains:val` on arrays | Wrong result (string-only!) | foreach + conditional |
| `\|transform_timestamp` | Returns "0" | `\|format_timestamp` |
| `\|delete:"key"` | Invalid filter name | `\|unset:"key"` |
| `\|implode:sep` | Invalid filter name | `\|join:sep` |
See [filters-reference.md](resources/filters-reference.md) for complete verified list + all non-existent filters.
**Chain for performance (600% faster than multiple var.update):**
```xanoscript
var $obj { value = {}|set:"a":$x|set:"b":$y|set:"c":$z }
```
See [filters-reference.md](resources/filters-reference.md) for complete list.
---
## Layer 8: API Requests & Function Calls
### api.request
```xanoscript
var $auth_header { value = "Authorization: Bearer "|concat:$env.API_KEY }
api.request {
url = "https://api.example.com/endpoint"
method = "POST" // Can also be a variable: method = $input.http_method
params = $request_body // PARAMS not body!
headers = []|push:$auth_header|push:"Content-Type: application/json"
timeout = 120
} as $result
// Response is NESTED - check timeout FIRST
conditional {
if ($result.response.result == false) {
throw { name = "TIMEOUT" value = "API call timed out" }
}
}
var $data { value = $result.response.result }
var $status { value = $result.response.status }
```
**Input defaults gotcha:** URLs and strings with special chars (`://`, `?`, `&`) CANNOT be used as input defaults (`text url?=https://...` causes error 524). Make such inputs required instead.
### function.run
```xanoscript
function.run "Folder/FunctionName" {
input = {
param1: $value1
param2: "literal"
}
} as $result
```
### Security Methods (Production-Verified Names)
```xanoscript
security.check_password {
text_password = $input.password
hash_password = $user.password
} as $pass_result
// CRITICAL: Uses `table =` NOT `auth =`. ALL 4 blocks REQUIRED.
security.create_auth_token {
table = "🔐 [AUTH] users" // Exact table name (including emoji if any!)
id = $user.id
extras = {} // Custom JWT claims: {}|set:"role":"admin"
expiration = 604800 // seconds (604800 = 7 days)
} as $authToken
```
See [security-methods.md](resources/security-methods.md) for full auth patterns.
### Utility Functions (runtime-verified)
```xanoscript
// Get ALL user-defined env vars as one object (does NOT include system env vars)
util.get_env {} as $all_env
var $api_key { value = $all_env|get:"openai_secret":"" }
// Get ALL raw input including undeclared query params
util.get_input {} as $raw_input // {test_param: "hello", foo: "bar"}
// Get only declared inputs (matching input {} block)
util.get_all_input {} as $declared // {test_param: "hello"}
// Get all in-scope variables
util.get_vars {} as $all_vars
// Set response headers
util.set_header { name = "X-Custom", value = "test" }
// Pause execution (value in milliseconds)
util.sleep { value = 2000 }
// IP geolocation
util.ip_lookup { ip = "8.8.8.8" } as $geo
// Distance between coordinates
util.geo_distance { lat1 = 40.7, lon1 = -74.0, lat2 = 34.0, lon2 = -118.2 } as $dist
```
**`util.get_input` vs `util.get_all_input`:** `get_input` returns ALL query params/body data (even undeclared ones). `get_all_input` returns ONLY params matching the `input {}` block.
---
## Layer 9: Workflow
```
1. Write MINIMAL XanoScript (one operation)
2. Deploy to Xano
3. Curl test IMMEDIATELY
4. Add ONE feature
5. Redeploy (NEVER create duplicates — update the existing endpoint!)
6. Curl test again
7. REPEAT until complete
```
**NEVER build complete solution first. NEVER skip curl testing.**
See [workflow.md](resources/workflow.md) and [curl-testing.md](resources/curl-testing.md).
---
## Navigation Guide
| Need to... | Read this |
|---|---|
| Copy-paste complete patterns (CRUD, auth, AI, webhooks) | [recipes.md](resources/recipes.md) |
| Check ALL valid values for any field | [closed-sets.md](resources/closed-sets.md) |
| See what DOESN'T work | [what-doesnt-work.md](resources/what-doesnt-work.md) |
| Full database operation reference | [database-operations.md](resources/database-operations.md) |
| Auth, password, token patterns | [security-methods.md](resources/security-methods.md) |
| All filter names and gotchas | [filters-reference.md](resources/filters-reference.md) |
| Build-test-deploy process | [workflow.md](resources/workflow.md) |
| Curl testing patterns | [curl-testing.md](resources/curl-testing.md) |
---
**Status**: PRODUCTION-READY | **Design**: Closed-set architecture - never guess, always verify
**Source**: Runtime-verified from extensive stress testing across production workspaces