Playground · Ktor Server

Introduction

Xiao DSL for Ktor Server is a YAML-inspired language for defining what happens when an HTTP client hits a route on the Ktor Server Playground tool.

You paste DSL into a route's Response body field in the Xiao Android app. The on-device server parses the text, runs optional database and network steps, resolves dynamic tokens, and returns an HTTP response.

What You Can Build

  • REST APIs backed by SQLite on the phone (CRUD without a laptop server).
  • Endpoints that expose live device data (battery, Wi-Fi IP, model, Android version).
  • Validation gates that return HTTP 400 with custom error messages.
  • Proxies that forward GET requests to external URLs and return the body.
  • Simulated slow APIs using delay: steps.
  • Counters and flags using the built-in kv_store key-value table.

Three Blocks

BlockRequiredRole
@settings:NoSQLite table configuration and error policy
@execute:NoOrdered list of side effects before responding
@return:NoHTTP status, Content-Type, headers, and body sent to the client

All three blocks are optional. An empty @return: with defaults yields HTTP 200 and an empty body.

DSL vs Plain Text

If the response body does not contain a line matching @settings:, @execute:, or @return: as block headers, the server treats the entire field as plain text: tokens are resolved once, and Content-Type is guessed from the first character ({ or [ = JSON, < = HTML, else text/plain).

Tokens use double curly braces: {{token_name}}. This manual is aligned with Xiao 1.1.0.

Getting Started

From zero to a working endpoint in Xiao.

Where to Write DSL

  1. Open Playground → Ktor Server.
  2. Stop the server if it is running (routes cannot be edited while running).
  3. Tap Add Route.
  4. Choose method and path (path must start with / and cannot be only /).
  5. Paste DSL into Response body. Tap See Help for the in-app copy of this reference.
  6. Save, enable the server, call http://<phone-ip>:<port><path> from another device.

Minimal Examples

Plain JSON ping (no blocks)

{"ok": true, "server_time": "{{datetime}}", "ip": "{{Wi-Fi_ip}}"}

DSL echo (return only)

@return:
  status: 200
  content_type: application/json
  body: {"echo": "{{request.body}}"}

Save JSON body to SQLite

@settings:
  table: events
  primary_key: id
@execute:
  - set: [id, payload] from request.body
  - set: id = {{uuid}}
@return:
  status: 201
  body: {"saved": true, "id": "{{col.id}}"}
The JSON / Plain Text / HTML selector in Add Route is a display badge only. The real Content-Type comes from content_type: in @return or from plain-text sniffing.

Request Processing Pipeline

Exact order of operations per HTTP request.

  1. Read body — Request body is read once into a cached string.
  2. Detect mode — If no block headers exist, resolve tokens on raw body and respond. Stop.
  3. Parse DSL — Split into @settings, @execute, @return. Parse failure returns HTTP 500 JSON error.
  4. Prepare table — If @settings names a table, ensureTable runs.
  5. Run @execute — Each directive runs top to bottom in a single coroutine.
  6. Flush writes — If table is set and set: steps accumulated columns, ensure columns then INSERT/UPSERT.
  7. Build @return body — Substitute tokens via DSLVarResolver and DynamicResponseResolver.
  8. Response headers — Each @return headers: entry resolved.
  9. Respond — Send body with status and content_type from @return.

Token Resolution Order

OrderTokens / features
1{{timestamp}}, {{datetime}}, {{random}}, {{uuid}}
2{{device_model}}, {{device_manufacturer}}, {{android_version}}, {{sdk_int}}
3{{battery_level}}, {{battery_status}}, {{battery_voltage}}
4{{Wi-Fi_ssid}}, {{Wi-Fi_rssi}}, {{Wi-Fi_ip}}
5{{request.path}}, {{request.method}}, {{request.uri}}, {{request.param.*}}, {{request.header.*}}, {{request.body}}, {{request.body.*}}
6{{db.set.*}}, {{db.incr.*}}, {{db.decr.*}}, {{db.del.*}}, {{db.get.*}}, {{db.count.*}}
7{{eval: ...}} then {{if: ... | ... | ...}}

Write Mode After set: Steps

Directive seenSQLite write verb
(none, default)INSERT OR REPLACE (upsert)
upsert: trueINSERT OR REPLACE
upsert: falseINSERT (fails on duplicate primary key)
insert: trueINSERT
insert: ignoreINSERT OR IGNORE

Plain Text Mode

Responses without DSL block headers.

Plain mode activates when the response body lacks any line matching @settings:, @execute:, or @return: as a block header.

Content-Type Sniffing

Body starts withContent-Type
{ or [application/json
<text/html
Anything elsetext/plain

All DynamicResponseResolver tokens are supported. No @execute (no validate, query, fetch, or automatic table writes from set:).

<html><body><h1>Device</h1><p>IP: {{Wi-Fi_ip}}</p><p>Battery: {{battery_level}}%</p></body></html>

@settings Block

Database binding and error policy.

Header line must be exactly @settings:. Following lines are key: value pairs. Leading - on keys is allowed.

Keys Reference

KeyDefaultBehavior
table(none)SQLite table name for writes
create_if_missingtrueCREATE TABLE IF NOT EXISTS when true
primary_keyidColumn name for CREATE TABLE and upsert conflict target
on_errorrespondrespond: HTTP 500 JSON on execute errors. silent: skip error response and build @return with empty execute results.
@settings:
  table: users
  create_if_missing: true
  primary_key: email
  on_error: respond

@execute Block

Every directive in depth. Header: @execute:. Each directive is one line; leading - is optional.

set: and upsert/insert

DirectiveDescription
set: [col1, col2] from request.bodyMap JSON object keys to columns
set: [col1, col2] from request.paramsMap query string parameters
set: [col1, col2] from request.headersMap HTTP header values
set: column = valueSingle column; value is token-resolved
upsert: true | falseINSERT OR REPLACE vs plain INSERT
insert: true | ignorePlain INSERT or INSERT OR IGNORE
delete: where ...DELETE FROM table WHERE predicate
update: SET ... where ...UPDATE with token-resolved SET and WHERE
query: SELECT ... [into var]First row only; use {{query.var.col}}
query_all: SELECT ... into varAll rows as JSON array in {{query_all.var}}
validate: expr | messageHTTP 400 if expression is empty, 0, or false
delay: msSuspend 0–30000 ms for latency simulation
local: key = valueScratch variable at {{local.key}}
fetch: URL [into var]Outbound HTTP GET with optional headers block
@execute:
  - validate: {{request.body.email}} | email is required
  - set: [email, name] from request.body
  - set: created_at = {{datetime}}
  - upsert: false
  - query: SELECT name FROM users WHERE email = '{{request.param.email}}' into u
  - fetch: https://api.example.com/data into data
    headers:
      Authorization: Bearer {{request.header.Authorization}}

@return Block

Status, headers, and body.

FieldDefaultNotes
status:200Integer HTTP status
content_type:application/jsonPassed to ContentType.parse
headers:(block)Indented key: value lines
body:(empty)Inline value or body: | for multiline
@return:
  status: 201
  content_type: application/json
  headers:
    X-Request-Id: {{uuid}}
    X-Battery: {{battery_level}}
  body: |
    {
      "id": "{{last_id}}",
      "email": "{{col.email}}",
      "at": "{{datetime}}"
    }

Token Encyclopedia

Every {{token}} supported by the engine. Tokens are case-sensitive.

Execute / Write Results

TokenWhen availableValue
{{last_id}}After insert/upsert flushSQLite rowid; blank if -1
{{col.NAME}}After flushString written to column NAME
{{query.VAR.COL}}After query: into VARColumn from first row
{{query.VAR}}After query: into VARJSONObject of first row
{{query_all.VAR}}After query_all: into VARJSONArray of all rows
{{local.KEY}}After local: KEY =Scratch value
{{fetch.VAR}}After fetch: into VARRaw response body
{{fetch.VAR.a.b}}JSON responseDot-path into parsed JSON

Time, Device, Battery, Wi-Fi

TokenResolves to
{{timestamp}}Unix milliseconds
{{datetime}}yyyy-MM-dd HH:mm:ss
{{uuid}}Random UUID v4
{{random}}Random integer 0–999999
{{device_model}}Build.MODEL
{{device_manufacturer}}Build.MANUFACTURER
{{android_version}}Build.VERSION.RELEASE
{{sdk_int}}API level
{{battery_level}}Percentage 0–100, or -1
{{battery_status}}charging, full, discharging, not_charging, unknown
{{battery_voltage}}Volts with two decimal places
{{Wi-Fi_ssid}}SSID with quotes stripped, or unknown
{{Wi-Fi_rssi}}RSSI dBm as string
{{Wi-Fi_ip}}IPv4 from Wi-FiManager DHCP

HTTP Request & Key-Value Store

TokenEffect / Value
{{request.body}}Cached raw body
{{request.body.FIELD}}Top-level JSON string field
{{request.path}}URI path before ?
{{request.method}}GET, POST, PUT, DELETE, etc.
{{request.uri}}Path and query string
{{request.param.NAME}}Query parameter (first value)
{{request.header.NAME}}Header value (case-sensitive)
{{db.get.KEY}}Read kv_store value or empty
{{db.set.KEY=VALUE}}Write VALUE; expands to empty
{{db.incr.KEY}}Atomic increment; returns new value
{{db.decr.KEY}}Atomic decrement
{{db.del.KEY}}Delete key
{{db.count.TABLE}}COUNT(*) on TABLE; 0 on error

Expressions: {{eval:}} and {{if:}}

Inline logic and arithmetic.

{{eval: EXPRESSION}}

Supports numbers, quoted strings, boolean literals, operators + - * / %, comparisons, logical && || !, and parentheses.

{{eval: {{battery_level}} * 2}}
{{eval: (10 + 5) * 3}}
{{eval: "active" == "active"}}

{{if: EXPR | WHEN_TRUE | WHEN_FALSE}}

Separator between parts is space-pipe-space: |. WHEN_FALSE is optional (defaults to empty).

{{if: {{battery_level}} < 20 | low | ok}}
{{if: {{request.param.verbose}} | debug_on}}
Do not use | inside EXPR without careful quoting; the parser splits on | with limit 3.

SQLite Database

Schema, kv_store, and SQL Runner.

Database file: ktor_server.db (version 1), singleton per app process.

Built-in kv_store Table

CREATE TABLE kv_store (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL DEFAULT '')

Powers all {{db.*}} tokens. Survives across routes and server restarts.

SQL Runner (Playground UI)

Statement typeResult UI
SELECT, PRAGMA, EXPLAIN, WITHColumn/row table
INSERT, UPDATE, DELETE, etc.Verb and rows affected count
Invalid SQLError message in table

Errors and HTTP Status Codes

ConditionHTTPBody shape
Plain mode success200Resolved plain/JSON/HTML
DSL success@return status@return body
DSL parse error500{"error":"DSL parse error: ..."}
validate: failed400{"error":"your message"}
Execute error + on_error respond500{"error":"Execute error: ..."}
Execute error + on_error silent@return status@return body (empty execute vars)
Return build error500{"error":"Return template error: ..."}

Recipes and Examples

Copy-paste endpoints for common patterns.

1. POST /register — create user with validation

@settings:
  table: users
  primary_key: email
  on_error: respond
@execute:
  - validate: {{request.body.email}} | email is required
  - set: [email, name] from request.body
  - set: registered_at = {{datetime}}
  - upsert: false
@return:
  status: 201
  body: {"message": "registered", "email": "{{col.email}}"}

2. GET /user?email= — lookup single row

@settings:
  table: users
  create_if_missing: false
@execute:
  - validate: {{request.param.email}} | email param required
  - query: SELECT name, email FROM users WHERE email = '{{request.param.email}}' into u
@return:
  body: |
    {"name": "{{query.u.name}}", "email": "{{query.u.email}}"}

3. GET /status — device telemetry, no table

@return:
  headers:
    X-Request-Id: {{uuid}}
  body: |
    {
      "battery": {{battery_level}},
      "ip": "{{Wi-Fi_ip}}",
      "model": "{{device_model}}",
      "time": "{{datetime}}"
    }

4. DELETE /user?id= — delete with silent errors

@settings:
  table: users
  on_error: silent
@execute:
  - validate: {{request.param.id}} | id is required
  - delete: where id = '{{request.param.id}}'
@return:
  status: 200
  content_type: text/plain
  body: deleted

5. GET /proxy?url= — forward external GET

@execute:
  - validate: {{request.param.url}} | url param is required
  - fetch: {{request.param.url}} into data
@return:
  body: {{fetch.data}}

6. GET /users — list table as JSON array

@execute:
  - query_all: SELECT id, name, email FROM users ORDER BY name into rows
@return:
  body: {"users": {{query_all.rows}}, "count": {{db.count.users}}}

7. POST /counter — atomic visit counter

@return:
  body: {"visits": {{db.incr.page_views}}}

8. GET /toggle — flip kv_store flag

@return:
  body: |
    {
      "was": "{{db.get.feature}}",
      "now": "{{db.set.feature={{if: {{db.get.feature}} | 0 | 1}}}}"
    }

9. GET /slow — artificial latency

@execute:
  - delay: 2000
@return:
  body: {"ready": true, "delayed_ms": 2000}

10. PUT /item — update with validation

@settings:
  table: items
  primary_key: id
@execute:
  - validate: {{request.param.id}} | id required
  - update: title = '{{request.body.title}}', updated = '{{datetime}}' where id = '{{request.param.id}}'
@return:
  body: {"updated": true, "id": "{{request.param.id}}"}

11. GET /health — combined eval and battery

@return:
  body: |
    {
      "ok": {{if: {{battery_level}} > 10 | true | false}},
      "level": {{battery_level}},
      "double": {{eval: {{battery_level}} * 2}}
    }

Troubleshooting

SymptomLikely causeFix
500 DSL parse errorTypo in @block header or directive syntaxMatch grammar in @execute; no PATCH in route UI
400 with custom messagevalidate: failedCheck param/body tokens resolve non-empty
500 Execute errorSQL, missing table, fetch failedSQL Runner test query; create_if_missing; check URL
Empty {{query.*}}No matching rowVerify SELECT and param tokens
Empty {{fetch.x.y}}Not JSON or wrong pathUse {{fetch.var}} first; validate JSON shape
Wrong Content-TypeUsed route badge not @returnSet content_type: in @return
Duplicate key on insertupsert: false with existing PKUse upsert: true or insert: ignore
Column not createdset: from body field missing in JSONAdd set: col = value or fix JSON keys
on_error silent but odd bodyExecute failed earlyCheck Logs; fix SQL; use respond for debugging

Enable Verbose logging in Xiao Settings and filter Logs by tag KtorServer for per-request traces.

Quick Reference Card

One-page syntax lookup.

Block headers

@settings:
@execute:
@return:

Execute directives

set: [a,b] from request.body|request.params|request.headers
set: col = value
upsert: true|false
insert: true|ignore
delete: where ...
update: SET ... where ...
query: SELECT ... [into var]
query_all: SELECT ... into var
validate: expr | message
delay: ms
local: key = value
fetch: URL [into var]
  headers:
    Name: value

@return fields

status: 200
content_type: application/json
headers:
  X-Custom: value
body: text
body: |
  multiline

@settings keys

table: name
create_if_missing: true|false
primary_key: id
on_error: respond|silent

Author: Saurav Sajeev (Developer) · Document date: June 04, 2026