RestermScript Technical Reference

May 6, 2026 ยท View on GitHub

RestermScript (or RTS which you will see quite often throughout the docs) is Resterm's built in expression language for templates, directives, and reusable modules. It is designed to be small, bounded, and easy to review inside request files. JavaScript via Goja is still available, but RestermScript is the preferred option when you want predictable behavior, clear errors, and safe execution.

Why this even exists

  • RTS is bounded and predictable because expressions run with strict step limits, cannot perform network operations or file writes, and only read files via json.file when file access is enabled.
  • RTS is safe because it avoids arbitrary evaluation and does not expose system APIs.
  • RTS is clear because the syntax is small and purpose built for request files.
  • RTS is debuggable because errors include file, line, and column information along with a call stack.

When to use it

Use RestermScript when you need small, safe logic for request evaluation and control flow.

  • Template values such as {{= expr }} are a good fit when you want computed headers, URLs, or JSON bodies.
  • Request and workflow control directives such as @when, @skip-if, @if, @switch, and @for-each can be driven by RestermScript expressions.
  • Assertions using @assert are readable and produce clear failures.
  • Reusable .rts modules imported with @use let you share logic across requests without bringing in JavaScript.

Use JavaScript only when you need full language features or when porting existing logic is not worth the rewrite.

Where it runs

  1. Templates
Authorization: Bearer {{= vars.get("auth.token") ?? env.get("auth.token") }}

Templates evaluate expressions and insert their string results into request fields. They are read only and should not cause side effects.

  1. Directives
# @when env.has("feature")
# @assert response.statusCode == 200

Directives evaluate expressions to decide whether a request runs or whether an assertion passes. They are read only and should not mutate request state.

  1. Modules
# @use ./rts/helpers.rts
# @use ./rts/helpers.rts as helpers

Modules are compiled once and expose only exported names through the alias (explicit or module name). Modules execute with rts; stdlib remains as a deprecated alias. When the host provides a request object it is available (read-only outside pre-request scripts). Modules do not automatically see env, vars, last, response, trace, or stream, so pass values into module functions explicitly.

  1. Apply patches
# @apply {headers: {"X-Test": "1"}}

Apply patches evaluate a single RestermScript expression that returns a patch dict and applies it to the outgoing request. They run before pre-request scripts and use read-only request and vars objects.

  1. Pre request scripts
# @rts pre-request

Pre-request scripts run full RestermScript blocks and can mutate the outgoing request and variables. They run before JavaScript pre-request blocks. The full # @script pre-request lang=rts form remains supported. Use @assert for RestermScript response checks.

Language overview

Comments

# starts a comment that runs to the end of the line. It can appear after whitespace or code.

Blocks and statement endings

Blocks use { ... } and group statements together. A newline can end a statement when the previous token can finish a statement. Newlines inside () and [] are ignored. The language also accepts the semicolon token as a statement terminator, but this guide uses newlines for clarity.

Identifiers and keywords

Identifiers start with a letter or _ and can contain letters, digits, and _ characters. The language reserves keywords and they cannot be used as identifiers.

Keywords:

export module fn let const if elif else try return for break continue range
true false null and or not

Literals

null
true / false
123  3.14
"string"  'string'
[1, 2, 3]
{a: 1, "b": 2}

String escapes include \n, \r, \t, \\, \", and \'. Dict keys in literals are identifiers or quoted strings, and dict keys are always strings at runtime.

Operators by precedence

  • Postfix operators include function calls, indexing, and member access.
  • Unary operators include not, try, and unary -.
  • Multiplicative operators include *, /, and %.
  • Additive operators include + and -.
  • Comparison operators include <, <=, >, and >=.
  • Equality operators include == and !=.
  • Logical operators include and and or.
  • The coalesce operator ?? returns the right side when the left side is null.
  • The ternary operator cond ? a : b selects between two values.

+ adds numbers or concatenates strings. Non numeric values are converted to string using str(). Comparisons only work for numbers or strings, and equality only works for primitive types.

Error handling with try

try expr

The try operator evaluates its expression and returns an object with ok, value, and error fields. ok is true on success and false on error. value holds the result on success and is null on error. error is a single line error string on failure and null on success. It does not catch hard aborts such as step limits, timeouts, or cancellations. You can use try expr directly in conditionals, but checking r.ok is often clearer.

Example:

let r = try json.file("_data/users.json")
if not r.ok { return [] }
return r.value

Use in .http expressions and directives:

# @when try json.file("_data/flags.json")
# @for-each ((try json.file("_data/users.json")).value ?? []) as user
# @assert try response.json("data")

Authorization: Bearer {{= (try response.json("auth.token")).value ?? "" }}

This pattern is most useful for optional files, optional JSON bodies, or helper calls that may fail.

Types and truthiness

RTS has several runtime types.

  • Null represents the absence of a value.
  • Bool represents true or false.
  • Number uses float64 for numeric values.
  • String stores UTF 8 text.
  • List stores ordered values.
  • Dict stores key value pairs.
  • Function represents a callable value.
  • Object represents host objects provided by Resterm.

Truthiness follows consistent rules. Null, false, zero, the empty string, the empty list, and the empty dict are false. All other values are true unless a host object defines custom truthiness (for example, try results are truthy only when ok is true).

Indexing and member access

List indexing uses numeric indices such as list[0], and out of range accesses return null. Dict access uses dict["key"] or dict.key, and missing keys return null. Object member access is supported, while indexing depends on the object implementation.

Statements

let and const

let name = expr
const name = expr

let creates a mutable binding and const creates an immutable binding. Redeclaring a name in the same scope is an error, while shadowing a name in an inner block is allowed. Assignment requires the name to exist in the current or parent scope.

Assignment

name = expr

Assignment only applies to variable names. Member assignment and index assignment are not supported.

Functions

fn add(a, b) {
  return a + b
}

Functions close over their lexical environment. Function names are immutable because fn defines a constant binding. Function parameters are local variables and can be reassigned.

Conditionals

if cond {
  ...
} elif other {
  ...
} else {
  ...
}

Conditionals evaluate each branch in order and execute the first branch whose condition is true. The else branch runs only when no earlier condition is true.

for loops

RTS supports several loop forms.

for { ... }
for cond { ... }
for let k, v range expr { ... }

The language also supports a three clause loop with init, condition, and post clauses. The clauses are separated by the semicolon token.

Rules for loops are consistent. break and continue are valid only inside loops. const is not allowed in loop headers. for let introduces loop scoped variables that do not escape the loop block. for range without let assigns to existing variables.

range semantics

Range iteration is deterministic and follows clear rules.

  • When you range a list, the key is the index and the value is the item.
  • When you range a dict, the key is the string key and the value is the item, and keys are sorted to keep output stable.
  • When you range a string, the key is the byte index and the value is a single rune string.

Example:

for let i, ch range "go" {
  // i is the byte index, ch is "g" and then "o"
}

Modules and exports

Modules are .rts files and they are imported with @use.

  • module <name> declares the module name (required when importing without as, and it must be the first statement).
  • export exposes a name from a module.
  • @use ./path.rts or @use ./path.rts as alias imports a module into a request or file.
  • If you omit as, the module name becomes the alias.
  • Modules are cached, so top level mutable state can persist across runs.

Example:

// helpers.rts
module helpers
export fn authHeader(token) {
  return token ? "Bearer " + token : ""
}
# @use ./rts/helpers.rts
Authorization: {{= helpers.authHeader(vars.get("auth.token")) }}

Modules run with rts only. The request object is available when the host provides it, but env, vars, last, response, trace, and stream are not. Pass values in as arguments when you need extra context. stdlib remains available as a deprecated alias.

Standard library

RTS provides a small standard library that covers common request needs without enabling file writes or network access. It keeps expressions small, readable, and predictable. The standard library is available as rts; stdlib remains as a deprecated alias. Core helpers and namespaces (crypto, base64, url, time, json, headers, query, encoding) are also exposed at top level for convenience. text, list, dict, and math are available only under rts.

Core helpers

  • rts.fail(msg) stops evaluation and returns an error message.
  • rts.len(x) returns the length of a string, list, or dict.
  • rts.contains(haystack, needle) checks whether a value is contained in a string, list, or dict.
  • rts.match(pattern, text) applies a regular expression to text and returns true when it matches.
  • rts.str(x) converts a value to a string, using JSON for lists and dicts.
  • rts.default(a, b) returns a unless it is null, otherwise it returns b.
  • rts.num(x[, def]) converts a value to a number, or returns def when conversion fails.
  • rts.int(x[, def]) converts a value to an integer, or returns def when conversion fails.
  • rts.bool(x[, def]) converts a value to a bool, or returns def when conversion fails.
  • rts.typeof(x) returns the type name.
  • rts.uuid() generates a UUID and requires random generation to be enabled.

Crypto helpers

  • rts.crypto.sha256(text) returns a hex encoded SHA-256 digest.
  • rts.crypto.hmacSha256(key, text) returns a hex encoded HMAC-SHA256 digest.

Encoding and URL helpers

  • rts.base64.encode(x) encodes a string to base64.
  • rts.base64.decode(x) decodes a base64 string.
  • rts.encoding.hex.encode(x) encodes a string to hex.
  • rts.encoding.hex.decode(x) decodes a hex string.
  • rts.encoding.base64url.encode(x) encodes a string to base64url (no padding).
  • rts.encoding.base64url.decode(x) decodes a base64url string.
  • rts.url.encode(x) percent encodes a string for URL use.
  • rts.url.decode(x) decodes a percent encoded string.

Time helpers

  • rts.time.nowISO() returns the current time in ISO 8601 format.
  • rts.time.nowUnix() returns the current time as unix seconds.
  • rts.time.nowUnixString() returns the current time as a decimal unix seconds string.
  • rts.time.nowUnixMs() returns the current time as unix milliseconds.
  • rts.time.format(layout) formats the current time with the given layout string.
  • rts.time.parse(layout, value) parses the time string and returns unix seconds (fractional).
  • rts.time.formatUnix(ts, layout) formats a unix timestamp with the given layout.
  • rts.time.addUnix(ts, secondsOrDuration) adds seconds (number) or a duration string to a unix timestamp.
  • rts.time.duration(value) parses a duration string (including d and w) and returns seconds.

JSON helpers

  • rts.json.file(path) reads and parses JSON using the request base directory (only when file access is enabled).
  • rts.json.parse(text) parses a JSON string into RestermScript values.
  • rts.json.stringify(value[, indent]) converts a value to JSON text. indent can be a string or a number (0-32).
  • rts.json.get(value[, path]) returns the value at a dot or [index] path (optional leading $) and returns null when missing.
  • rts.json.has(value, path) returns true when a value exists at the path.

Text helpers

  • rts.text.lower(s) returns a lowercased string.
  • rts.text.upper(s) returns an uppercased string.
  • rts.text.trim(s) trims leading and trailing whitespace.
  • rts.text.split(s, sep) splits a string into a list.
  • rts.text.join(list, sep) joins list items with a separator (items may be strings, numbers, or bools).
  • rts.text.replace(s, old, new) replaces all occurrences of old with new.
  • rts.text.startsWith(s, prefix) returns true when a string starts with prefix.
  • rts.text.endsWith(s, suffix) returns true when a string ends with suffix.

List helpers

  • rts.list.append(list, item) returns a new list with item appended.
  • rts.list.concat(a, b) returns a new list with b appended to a.
  • rts.list.sort(list) returns a sorted copy (numbers or strings only).
  • rts.list.map(list, fn) returns a new list with fn(item) applied to each value.
  • rts.list.filter(list, fn) returns a new list of values where fn(item) is truthy.
  • rts.list.any(list, fn) returns true if any value makes fn(item) truthy.
  • rts.list.all(list, fn) returns true if all values make fn(item) truthy.
  • rts.list.slice(list, start[, end]) returns a slice of the list.
  • rts.list.unique(list) returns a list of unique primitive values.

Dict helpers

  • rts.dict.keys(dict) returns a sorted list of keys.
  • rts.dict.values(dict) returns values ordered by sorted keys.
  • rts.dict.items(dict) returns a list of {key, value} entries ordered by key.
  • rts.dict.set(dict, key, value) returns a new dict with key set.
  • rts.dict.merge(a, b) returns a new dict with b applied over a.
  • rts.dict.remove(dict, key) returns a new dict without key.
  • rts.dict.get(dict, key[, def]) returns def or null when missing.
  • rts.dict.has(dict, key) returns true when a key exists.
  • rts.dict.pick(dict, keys) returns a dict with the specified keys.
  • rts.dict.omit(dict, keys) returns a dict without the specified keys.

Math helpers

  • rts.math.abs(x) returns the absolute value.
  • rts.math.min(a, b) returns the smaller value.
  • rts.math.max(a, b) returns the larger value.
  • rts.math.clamp(x, min, max) clamps x into the range.
  • rts.math.floor(x) returns the largest integer <= x.
  • rts.math.ceil(x) returns the smallest integer >= x.
  • rts.math.round(x) rounds to the nearest integer (half away from zero).

Host objects for request evaluation

Resterm exposes host objects when evaluating templates, directives, @apply, assertions, and pre-request scripts. In pre-request scripts, request and vars expose mutation helpers, while everything else is read-only. Lookups in env and vars are case-insensitive; header lookups are normalized, while query keys and JSON paths are case-sensitive.

env

env provides environment values. You can access values through env.get("name"), env.has("name"), env.require("name"[, msg]), or env.name. require throws when a value is missing.

vars

vars provides request runtime variables, including globals and workflow overrides. You can access values through vars.get("key"), vars.has("key"), vars.require("key"[, msg]), or vars.key. vars.global provides global reads and writes in pre-request scripts through get, has, require, set, and delete.

request

request provides a summary of the current request. It exposes method, url, headers, header(name), and query. headers contains the first value per header (lowercased keys), while header(name) is case-insensitive. query returns strings or lists when a key has multiple values. In @rts pre-request blocks, mutation helpers are available, including request.setMethod, request.setURL, request.setHeader, request.addHeader, request.removeHeader, request.setQueryParam, and request.setBody. The full @script pre-request lang=rts form is equivalent. In @apply, the request object is read only, so you return a patch dict instead of mutating it.

last

last provides a summary of the most recent response. It exposes status, statusCode, statusText, url, headers, header(name), text(), and json(path). headers contains the first value per header, while header(name) is case-insensitive. json(path) accepts a simple dot and [index] path (optional leading $) and returns null when a value is missing.

response

response provides a summary of the current response when evaluating @assert. It has the same shape as last.

trace

trace provides timing and budget information for the most recent response. It includes helpers such as trace.enabled(), trace.durationMs(), trace.durationSeconds(), trace.durationString(), trace.error(), trace.started(), trace.completed(), trace.phases(), trace.phaseNames(), trace.getPhase("dns"), trace.budgets(), trace.breaches(), trace.withinBudget(), and trace.hasBudgets().

stream

stream provides streaming metadata for SSE and WebSocket requests. It includes helpers such as stream.enabled(), stream.kind(), stream.summary(), and stream.events(). Summary and event shapes depend on the stream type (for SSE: eventCount, byteCount, duration, reason; for WebSocket: sentCount, receivedCount, duration, closedBy, closeCode, closeReason).

Directives and workflows

@use

# @use ./rts/helpers.rts
# @use ./rts/helpers.rts as helpers

@use is valid at file or request scope. If you omit as, the module name declared with module <name> becomes the alias.

@apply

# @apply {headers: {"Authorization": "Bearer " + vars.get("auth.token")}}
# @apply use=jsonApi,use=authProd

@apply is a request scoped directive and you can use it multiple times in a request. Each apply expression is evaluated in order before pre-request scripts. The expression must return a dict patch with specific keys.

You can also reference reusable named patches with use=. Comma-separated use= entries run left-to-right inside the same @apply line.

@patch

# @patch file jsonApi {headers: {"Accept":"application/json","Content-Type":"application/json"}}
# @patch global authProd {auth: {type:"oauth2", cache_key:"myapi"}}

@patch defines reusable patch expressions for @apply use=....

  • Scope must be file or global.

  • Resolution for @apply use=name is file scope first, then global scope.

  • Patch names are case-insensitive when resolving.

  • method expects a string and replaces the HTTP method, and Resterm uppercases it.

  • url expects a string and replaces the request URL.

  • headers expects a dict where values are strings, numbers, bools, or lists of those; null deletes a header.

  • query expects a dict where values are strings, numbers, or bools; null deletes the key.

  • body accepts any value. Strings are used as is, and other values are converted with str().

  • auth expects a dict with type plus optional params. Use null to clear auth for that run.

  • settings expects a dict where values are strings, numbers, or bools; null deletes a setting key.

  • vars expects a dict and sets request scope variables for this run (values are strings, numbers, or bools).

@when and @skip-if

# @when vars.has("auth.token")
# @skip-if env.mode == "dry-run"

These directives are evaluated before pre-request scripts. If the condition is false, the request is skipped and a reason is reported.

@assert

# @assert response.statusCode == 200
# @assert contains(response.header("Content-Type"), "json")

Each expression is evaluated and truthy means pass. Use response for the current request response.

@if, @elif, and @else

These directives are used in workflows to branch steps.

# @if last.statusCode == 200 run=StepOK
# @elif last.statusCode == 401 run=StepRefresh
# @else fail="unexpected status"

@switch, @case, and @default

# @switch last.statusCode
# @case 200 run=StepOK
# @case 401 run=StepRefresh
# @default fail="unexpected status"

@for-each

# @for-each json.file("_data/users.json") as user

The expression must evaluate to a list. It introduces a loop variable that you can use in RestermScript expressions. In workflows, it also sets vars.workflow.<name> and vars.request.<name> for legacy templates.

Limits and safety

RestermScript enforces hard limits to prevent runaway scripts and keep the UI responsive. These limits include maximum steps per evaluation, maximum call depth, maximum string size, maximum list size, maximum dict size, and an optional timeout. When a limit is exceeded, evaluation fails with a detailed error.

Common patterns

Guarded requests

# @when vars.has("auth.token")
GET {{base_url}}/bearer
Authorization: {{= "Bearer " + vars.get("auth.token") }}

Controlled branching in workflows

# @switch last.statusCode
# @case 401 run=Refresh
# @case 200 run=Upsert
# @default fail="unexpected status"

Reusable module logic

module users
export fn label(user) {
  return (user.name ?? "unknown") + " <" + (user.email ?? "n/a") + ">"
}
# @use ./rts/users.rts
X-User: {{= users.label(user) }}

Design constraints and why they exist

RestermScript prioritizes predictable evaluation and safe execution. It does not allow file writes or network access, and file reads are limited to json.file when enabled. It does not allow member assignment because it reduces side effects and simplifies the interpreter. It requires an explicit alias or module name to avoid name collisions and keep imports explicit. It keeps host objects read-only in most contexts because request evaluation should remain declarative. It sorts dict keys during range to keep iteration order deterministic across runs.

If you need full scripting or side effects, use JavaScript @script blocks. For everything else, RestermScript is the safer and more readable choice.