Script Language Reference

Complete reference for the ArchAstro script language — syntax, operators, namespaces, and patterns.

This reference is also available in the CLI via archastro script docs or archastro configs script-reference. This page is auto-generated from the platform source. Do not edit manually.

ArchAstro Script Language Reference

ArchAstro scripts are expression-oriented. Every statement produces a value. The last expression in a script is its return value. Statements are separated by semicolons or newlines (automatic semicolon insertion).

Comments

// line comment
/* block comment (nestable) */

Literals

  • Numbers: 42, 3.14, 1e-5
  • Strings: "hello" or 'hello' with escapes \n, \r, \t, \", \', \\
  • Booleans: true, false
  • Null: null
  • Arrays: [1, 2, 3]
  • Objects: {key: "value", "other_key": 42}

Truthy / Falsy

Only these values are falsy: false, null, 0, 0.0, "" (empty string), [] (empty array). Everything else is truthy, including empty objects {}.

Variables

let declares a binding. Variables are block-scoped. Rebinding a name in the same scope shadows the previous value.

let name = "world"
let count = 42
let items = [1, 2, 3]
let config = {key: "value", enabled: true}
let count = count + 1  // shadows previous count

Reserved names that cannot be used as variables: env, import, viewer, unwrap.

Operators

Precedence (highest to lowest):

  1. Member access: .property, [index]
  2. Function call: fn(args)
  3. Unary: !, -
  4. Multiplicative: *, /, %
  5. Additive: +, -
  6. Relational: <, <=, >, >=
  7. Equality: ==, !=
  8. Logical AND: &&
  9. Logical OR: ||
  10. Ternary: ? :
  11. Try-unwrap: postfix ?

Short-circuit operators

&& and || return actual values (not booleans), like JavaScript:

"hello" && "world"    // "world"
0 && "skipped"        // 0
null || "default"     // "default"
"found" || "fallback" // "found"

String concatenation uses +: "hello " + "world".

Conditional Expressions

if (condition) {
  thenValue
} else {
  elseValue
}

Conditionals are expressions that return a value:

let label = if (count > 10) { "many" } else { "few" }

Ternary shorthand: condition ? thenValue : elseValue

Important: } else must be on the same line to avoid automatic semicolon insertion.

// CORRECT
if (x) { 1 } else { 2 }

// CORRECT
if (x) {
  1
} else {
  2
}

// WRONG — ASI inserts semicolon after }
if (x) {
  1
}
else {
  2
}

Functions

Anonymous functions:

fn(x) { x * 2 }
fn(a, b) { a + b }

Named functions (desugars to let binding):

fn double(x) { x * 2 }
double(5) // 10

Functions are first-class values — they can be passed as arguments, returned from other functions, and stored in variables. Closures capture their lexical scope at definition time. Named functions support recursion.

// Recursion
fn factorial(n) {
  if (n <= 1) { 1 } else { n * factorial(n - 1) }
}
factorial(5) // 120

// Higher-order: passing functions as arguments
fn apply(f, x) { f(x) }
apply(fn(n) { n * 2 }, 5) // 10

// Closures capture outer scope
fn makeAdder(x) {
  fn(y) { x + y }
}
let add5 = makeAdder(5)
add5(10) // 15

// Most common pattern: inline anonymous functions with namespaces
array.filter(items, fn(item) { item.active })
array.map(items, fn(item) { item.name })
array.reduce(items, 0, fn(sum, item) { sum + item.price })

Functions enforce exact parameter count — calling with wrong arity is an error.

JSONPath Access

$ accesses the root input payload. @ is the current item in projections/filters.

$.user.name
$.items[0]
$.items[*].price
$.items[?(@.active == true)]

Namespace Imports

Import a namespace to access its functions:

let arr = import("array")
arr.map([1, 2, 3], fn(x) { x * 2 })

Or call namespace functions directly without import:

array.map([1, 2, 3], fn(x) { x * 2 })
string.uppercase("hello")

Result Type

Operations that can fail return Result values:

  • Ok: {"ok": true, "value": <data>}
  • Err: {"ok": false, "error": {"code": "error_code", "message": "description"}}

unwrap(result) — extracts value from Ok, halts script on Err. unwrap(result, default) — extracts value from Ok, returns default on Err. Postfix ? operator — unwraps Ok, early-returns Err from current function.

// Halt on error
let data = unwrap(http.get("https://api.example.com"))

// Provide fallback
let data = unwrap(http.get("https://api.example.com"), null)

// Early return in function
fn fetchUser(id) {
  let resp = http.get("https://api.example.com/users/" + id)?
  resp.body
}

Debugging

println(...) outputs values to the console panel. Takes any number of arguments.

println("user:", user)
println("count =", array.length(items))

Special Identifiers

  • $ — JSONPath root input. Use $.field to read from workflow input payload.
  • @ — JSONPath current item. Available inside JSONPath projections and filters.

Environment Variables

Apps can configure environment variables (secrets, API keys, configuration). These are injected into scripts as the env object. Access them with dot notation:

env.API_KEY
env.WEBHOOK_SECRET
env.BASE_URL

env is a reserved name — you cannot use it as a variable name. Environment variables are read-only. If no environment variables are configured, env is not available and accessing it will produce an error.

// Use env vars for secrets in HTTP requests
let http = import("requests")
let resp = unwrap(http.post(env.WEBHOOK_URL, {
  body: $.payload,
  headers: {"Authorization": "Bearer " + env.API_TOKEN}
}))
resp.body

Builtin Functions

  • contains(string, substring), contains(list, value) — Returns true if a string contains a substring or a list contains a value. → boolean
  • icontains(string, substring) — Case-insensitive substring check. → boolean
  • import(namespaceName) — Loads a namespace (array, map, string, math) and returns its function map. → namespace
  • lowercase(string) — Returns a lowercased string. → string
  • map(key1, value1, key2, value2, ...) — Builds a map from alternating key/value pairs. → map
  • merge(leftMap, rightMap) — Merges two maps. Keys in rightMap overwrite leftMap. → map
  • println(...) — No documentation available. → any
  • put(map, key, value) — Returns a map with key set to value. Nil map input is treated as empty map. → map
  • unwrap(result), unwrap(result, default) — Extracts the value from an Ok result. Halts with an error if the result is Err. With two arguments, returns the default value instead of halting on Err. → any

Namespaces

array

Array/list helpers.

  • array.concat(listA, listB) — Concatenates two lists. → list
  • array.every(list, fn(item) -> boolean) — Returns true when all items satisfy the predicate. → boolean
  • array.filter(list, fn(item) -> boolean) — Returns items where predicate is truthy. → list
  • array.find(list, fn(item) -> boolean) — Returns first item matching predicate or nil. → any | nil
  • array.first(list) — Returns first list item or nil. → any | nil
  • array.flat(list), array.flat(list, depth) — Flattens nested lists (all levels by default). → list
  • array.indexOf(list, value) — Returns index of value or -1 when not found. → integer
  • array.join(list), array.join(list, separator) — Joins list values into a string. → string
  • array.last(list) — Returns last list item or nil. → any | nil
  • array.length(list) — Returns list length. → integer
  • array.map(list, fn(item) -> value) — Transforms each list item with mapper function. → list
  • array.reduce(list, initial, fn(acc, item) -> nextAcc) — Reduces list into a single value. → any
  • array.reverse(list) — Returns a reversed list. → list
  • array.slice(list, start), array.slice(list, start, stop) — Returns list slice with stop treated as exclusive. → list
  • array.some(list, fn(item) -> boolean) — Returns true if at least one item satisfies predicate. → boolean

datetime

Date and time operations: parsing, formatting, arithmetic, comparison, and timezone conversion.

  • datetime.add(datetime, amount, unit) — Adds a duration to a datetime. Amount can be negative to subtract. Units: seconds, minutes, hours, days, weeks, months, years. → Result<string>
  • datetime.compare(a, b) — Compares two datetimes. Returns -1 if a < b, 0 if equal, 1 if a > b. → Result<number>
  • datetime.diff(a, b, unit) — Returns the difference between two datetimes (a - b) in the given unit. Units: seconds, minutes, hours, days, weeks. → Result<number>
  • datetime.format(datetime, pattern) — Formats a datetime using strftime patterns. Common: %Y (year), %m (month), %d (day), %H (hour), %M (minute), %S (second), %B (month name), %A (weekday name). → Result<string>
  • datetime.now(), datetime.now(timezone) — Returns the current time as an ISO 8601 string. Without arguments returns UTC. With a timezone (e.g. "America/Denver") returns local time with offset. → string (ISO 8601)
  • datetime.parse(string) — Parses a date or datetime string into a normalized ISO 8601 string. Accepts ISO 8601 dates ("2026-02-18"), datetimes ("2026-02-18T15:30:00Z"), and datetimes with offsets. → Result<string>
  • datetime.parts(datetime) — Decomposes a datetime into its component parts as a map. → Result<{year, month, day, hour, minute, second, weekday, iso}>
  • datetime.startOf(datetime, unit) — Truncates a datetime to the start of the given unit. Units: second, minute, hour, day, month, year. → Result<string>
  • datetime.toTimezone(datetime, timezone) — Converts a datetime to the specified timezone. Returns an ISO 8601 string with the timezone offset. → Result<string>
  • datetime.unix(), datetime.unix(datetime) — Returns a Unix timestamp (seconds since epoch). Without arguments returns the current UTC time. With a datetime string, converts it to a Unix timestamp. Useful for JWT iat/exp claims. → number (Unix timestamp in seconds)

email

Email sending and template rendering.

  • email.loadTemplate(template_id) — Loads an EmailTemplate config by ID, lookup_key, or virtual_path. Returns a Result containing the template fields. → Result<{id, name, html_template, text_template}>
  • email.render(template, variables) — Renders a loaded email template with the given variables using Liquid syntax. Returns a Result with rendered html and text strings. → Result<{html, text}>
  • email.send({to, subject, text_body, html_body?, cc?, bcc?, from_name?, from_email?, reply_to?}) — Sends an email. Required fields: to, subject, text_body. Optional: html_body (defaults to text_body), cc, bcc, from_name, from_email, reply_to. → Result<{status, to, subject}>

jwt

Create and sign JSON Web Tokens for service authentication.

  • jwt.decode(token) — Decodes a JWT and returns the payload claims WITHOUT verifying the signature. Use this to read claims from a token, not to validate it. → Result with claims map
  • jwt.sign(claims, secret, algorithm) — Signs a JWT with the given claims, secret/key, and algorithm. Supported algorithms: RS256 (RSA private key PEM), HS256 (shared secret string). Returns a Result — use unwrap() to get the token string. → Result with signed JWT string

map

Map/object helpers.

  • map.delete(object, key) — Returns map without the provided key. → map
  • map.entries(object) — Returns [[key, value], ...] pairs for a map. → list
  • map.filterKeys(object, fn(key) -> boolean) — Keeps entries whose key passes predicate. → map
  • map.fromEntries(entries) — Builds a map from [[key, value], ...] entries. → map
  • map.get(object, key), map.get(object, key, defaultValue) — Reads a value from a map with optional default fallback. → any
  • map.has(object, key) — Returns true when key exists in map. → boolean
  • map.keys(object) — Returns map keys. → list
  • map.mapValues(object, fn(value) -> newValue) — Transforms each value while preserving keys. → map
  • map.merge(left, right) — Merges two maps. Keys in right overwrite left. → map
  • map.put(object, key, value) — Returns map with key set to value. → map
  • map.size(object) — Returns map size. → integer
  • map.values(object) — Returns map values. → list

math

Math helpers.

  • math.abs(number) — Returns absolute value. → number
  • math.ceil(number) — Rounds number up to nearest integer. → integer
  • math.floor(number) — Rounds number down to nearest integer. → integer
  • math.max(a, b), math.max(list) — Returns maximum of two numbers or max element from list. → number
  • math.min(a, b), math.min(list) — Returns minimum of two numbers or min element from list. → number
  • math.pow(base, exponent) — Returns base raised to exponent. → number
  • math.round(number) — Rounds number to nearest integer. → integer
  • math.sqrt(number) — Returns square root of non-negative numbers. → number

persona_templates

Bound API namespace persona_templates.

  • persona_templates.install({user: ..., template: ...}) — Install a persona template for a user.

The template_id parameter accepts a persona template ID, key, or a config ID (uuid, public id, lookup key, or virtual path) for a stored PersonaTemplate config. → The installed persona

  • persona_templates.list({app: ...}) — List persona templates for an app → Result
  • persona_templates.show({app: ..., persona_template: ...}) — Show a single persona template → Persona template

personas

Bound API namespace personas.

  • personas.list({user: ..., filter: ...}) — List personas for a user → Result

requests

HTTP client for making requests to external APIs.

  • http.delete(url), http.delete(url, { headers?, query?, body?, timeout?, auth? }) — Makes an HTTP DELETE request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). → Result<{status, body, headers}>
  • http.get(url), http.get(url, { headers?, query?, body?, timeout?, auth? }) — Makes an HTTP GET request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). → Result<{status, body, headers}>
  • http.head(url), http.head(url, { headers?, query?, body?, timeout?, auth? }) — Makes an HTTP HEAD request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). → Result<{status, body, headers}>
  • http.patch(url), http.patch(url, { headers?, query?, body?, timeout?, auth? }) — Makes an HTTP PATCH request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). → Result<{status, body, headers}>
  • http.post(url), http.post(url, { headers?, query?, body?, timeout?, auth? }) — Makes an HTTP POST request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). → Result<{status, body, headers}>
  • http.put(url), http.put(url, { headers?, query?, body?, timeout?, auth? }) — Makes an HTTP PUT request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). → Result<{status, body, headers}>

result

Result type helpers. All functions handle non-Result inputs defensively (no crashes).

  • result.err(message), result.err(code, message) — Constructs an Err result with optional code. → Result
  • result.isErr(value) — Returns true if value is an Err result. Returns false for non-Result values. → boolean
  • result.isOk(value) — Returns true if value is an Ok result. Returns false for non-Result values. → boolean
  • result.map(result, fn(value) -> newValue) — Applies mapper to Ok value, returns Err unchanged. Returns non-Result values unchanged. → Result
  • result.ok(value) — Constructs an Ok result wrapping the given value. → Result
  • result.unwrapOr(result, default) — Returns the Ok value or the default. Returns default for non-Result values. → any

slack

Send messages to Slack channels via the agent's Slack bot integration.

  • slack.send({channel, text, thread_ts?}) — Posts a message to a Slack channel. Requires channel (e.g. "#alerts") and text. Optional thread_ts for replying in a Slack thread. The agent must have integration/slack_bot installed. → Result with {ok: true} or error

string

String helpers.

  • string.capitalize(value) — Uppercases the first character, leaves the rest unchanged. → string
  • string.charAt(value, index) — Returns the character at the given index, or null if out of bounds. Supports negative indices. → string | null
  • string.endsWith(value, suffix) — Checks whether value ends with suffix. → boolean
  • string.format(template, ...args) — C-style string formatting. Supported specifiers: %s (string), %d (integer), %f (float, 6 decimals), %.Nf (float with N decimal places), %j (compact JSON), %J (pretty-printed JSON), %% (literal %). Example: string.format("Hello %s, you are %d", name, age) → string
  • string.includes(value, substring) — Checks whether value contains substring. → boolean
  • string.indexOf(value, substring) — Returns the byte position of the first occurrence, or -1 if not found. → integer
  • string.lastIndexOf(value, substring) — Returns the byte position of the last occurrence, or -1 if not found. → integer
  • string.length(value) — Returns character count. → integer
  • string.lowercase(value) — Lowercases a string. → string
  • string.match(value, pattern) — Runs a regex pattern against the string. Returns the first match with index and capture groups, or null if no match. → {match, index, groups} | null
  • string.padEnd(value, targetLength), string.padEnd(value, targetLength, padString) — Pads the end of the string to the target length. Defaults to spaces. → string
  • string.padStart(value, targetLength), string.padStart(value, targetLength, padString) — Pads the start of the string to the target length. Defaults to spaces. → string
  • string.repeat(value, count) — Repeats the string count times. Max count is 10,000. → string
  • string.replace(value, pattern, replacement) — Replaces all occurrences of a literal pattern with replacement. → string
  • string.replacePattern(value, regexPattern, replacement) — Replaces all regex matches with replacement. Supports capture group backreferences (\1, \2). → string
  • string.reverse(value) — Reverses the string. → string
  • string.split(value, separator) — Splits a string into a list by separator. → list
  • string.startsWith(value, prefix) — Checks whether value starts with prefix. → boolean
  • string.substring(value, start), string.substring(value, start, length) — Returns a substring from start with optional length. → string
  • string.test(value, pattern) — Tests whether a regex pattern matches anywhere in the string. → boolean
  • string.toNumber(value) — Parses a string to a number (integer or float). Returns null if the string is not a valid number. → number | null
  • string.toString(value) — Converts any value to its string representation. Maps and lists are JSON-encoded. → string
  • string.trim(value) — Trims surrounding whitespace. → string
  • string.trimEnd(value) — Trims trailing whitespace. → string
  • string.trimStart(value) — Trims leading whitespace. → string
  • string.uppercase(value) — Uppercases a string. → string

threads

Bound API namespace threads.

  • threads.create({user: ..., thread: ..., skip_welcome_message: ...}) — Create a thread for a user → The created thread
  • threads.list({user: ..., filter: ...}) — List threads for a user → Result
  • threads.toggle_persona({user: ..., thread: ..., persona: ..., enabled: ...}) — Toggle a persona on/off for a user thread → 204 No Content

users

Bound API namespace users.

  • users.create({app: ..., email: ..., full_name: ..., org: ..., org_role: ..., is_system_user: ..., skip_onboarding: ...}) — Create a new user for an app → Created user
  • users.list({app: ..., page: ..., page_size: ..., search: ..., status: ..., is_system_user: ..., email: ..., org: ..., org_role: ...}) — List paginated users for an app → Result

Examples

Data transformation

let items = $.order.items
let arr = import("array")
let total = arr.reduce(items, 0, fn(sum, item) {
  sum + item.price * item.qty
})
{total: total, count: arr.length(items)}

Filtering and mapping

let users = $.users
let active = array.filter(users, fn(u) { u.status == "active" })
array.map(active, fn(u) {
  {name: string.uppercase(u.name), email: u.email}
})

Conditional logic with defaults

let role = $.user.role || "viewer"
let limit = if (role == "admin") { 1000 } else { 100 }
{role: role, limit: limit}

String formatting

let name = $.user.name
let count = array.length($.items)
string.format("Hello %s, you have %d items", name, count)

Error handling

let http = import("requests")
let resp = unwrap(http.get($.api_url), null)
if (resp) {
  resp.body
} else {
  {error: "request failed"}
}

Working with dates

let dt = import("datetime")
let now = dt.now()
let deadline = unwrap(dt.parse($.due_date))
let days_left = dt.diff(deadline, now, "days")
if (days_left < 0) {
  "overdue by " + string.toString(math.abs(days_left)) + " days"
} else {
  string.toString(days_left) + " days remaining"
}

Building maps dynamically

let entries = array.map($.fields, fn(f) {
  [f.key, string.trim(f.value)]
})
map.fromEntries(entries)

HTTP POST with headers

let http = import("requests")
let resp = unwrap(http.post("https://api.example.com/webhooks", {
  body: {event: "order.created", data: $.order},
  headers: {"X-Api-Key": $.api_key},
  timeout: 30
}))
resp.body

Regex matching

let email = $.user.email
if (string.test(email, "^[^@]+@[^@]+\\.[^@]+$")) {
  let parts = unwrap(string.match(email, "^([^@]+)@(.+)$"), null)
  if (parts) {
    {local: parts.groups[0], domain: parts.groups[1]}
  } else {
    {error: "parse failed"}
  }
} else {
  {error: "invalid email"}
}

Chained data pipeline

let orders = $.orders

// Filter → transform → aggregate
let result = array.filter(orders, fn(o) { o.status == "completed" })
let result = array.map(result, fn(o) {
  {id: o.id, total: o.price * o.qty, date: o.created_at}
})
let grandTotal = array.reduce(result, 0, fn(sum, o) { sum + o.total })

{orders: result, grand_total: grandTotal, count: array.length(result)}

Function composition pattern

fn pipe(value, fns) {
  array.reduce(fns, value, fn(acc, f) { f(acc) })
}

let result = pipe($.input, [
  fn(s) { string.trim(s) },
  fn(s) { string.lowercase(s) },
  fn(s) { string.replace(s, " ", "-") }
])
result