Scripts

Write focused custom logic for workflow steps, policy checks, adapters, and other places where built-in nodes are not enough.

Overview

Scripts are where you put the small pieces of custom logic that give a workflow or routine its project-specific behavior.

They are useful when the platform already gives you the overall structure, but you still need code for the part that is unique to your business.

Typical uses include:

  • reshaping data between steps
  • applying policy checks
  • adapting one system's format to another
  • making a routing decision that is too custom for a simple expression

Scripts are not "write arbitrary code everywhere." They are small, reviewable bits of custom logic inside an otherwise understandable flow.


Language basics

The ArchAstro script language is expression-oriented. The last expression in the script body is the return value -- there is no return keyword.

Key syntax rules:

  • Variables: let x = 10 (no const, var, or function keywords)
  • Anonymous functions: fn(x) { x * 2 }
  • Imports: import("array"), import("requests"), etc.
  • Input payload: $ gives access to the input data via JSONPath
  • Input declarations: input var_name declares variables from the execution environment (for workflow step outputs)
  • Environment variables: env.API_KEY, env.SLACK_WEBHOOK
  • Comments: // single-line and /* */ multi-line
  • Semicolons: optional (automatic semicolon insertion)
  • No loops: use array.map, array.filter, array.reduce instead of for or while

Available import namespaces: requests, array, string, map, datetime, math, result, email, jwt, slack. See the Script Language Reference for the full list of namespaces and functions.


A concrete example

Imagine a workflow that processes refund requests. Most of the workflow stays visual -- receive the request, gather account info, check approval, send the result. The script handles the custom part in the middle: calculate the refund, normalize billing data, enforce a business rule.

let http = import("requests")
let arr = import("array")

let items = $.order.line_items
let eligible = arr.filter(items, fn(item) { item.refundable == true })

let totals = arr.map(eligible, fn(item) {
  {
    sku: item.sku,
    refund_amount: item.price * item.quantity
  }
})

let grand_total = arr.reduce(totals, 0, fn(acc, t) { acc + t.refund_amount })

let approval = unwrap(http.post(env.BILLING_API_URL, {
  headers: { "Authorization": "Bearer " + env.BILLING_API_KEY },
  body: { order_id: $.order.id, amount: grand_total }
}))

{
  eligible_items: totals,
  total_refund: grand_total,
  approval_id: approval.body.id
}

That script reads the input payload with $, filters and transforms data with array functions, calls an external API with requests, and returns a structured object for the next workflow step.


Execution contexts

Where a script runs determines what $ contains and what capabilities are available.

Context $ contains env available Builtin tools available
Workflow ScriptNode Step input data Yes No
Routine handler (script type) Event payload Yes No
Custom tool script Tool arguments Yes No
do_task preset N/A (LLM has full tool access) Yes Yes (all agent tools)

Scripts run under the same scoped platform authorization model as the routine or workflow that invoked them.

Scripts can also use input var_name to declare named variables from the execution environment. This is useful when a workflow step outputs a named result that the next script needs to consume. Unknown identifiers are errors — declare them with let or input.


Scripts vs expressions

Feature Script Expression
Multi-step logic Yes No
Return value Last expression (implicit) Implicit evaluation
Imports Yes (import("namespace")) No
HTTP calls Yes (via requests) No
Error handling unwrap() builtin, result namespace Minimal
Use in workflows Full ScriptNode Inline conditions and field access
Best for Custom behavior, transformations Small checks, field access, routing guards

Use expressions when the logic is tiny and obvious -- a field comparison, a null check, simple string interpolation.

Use scripts when:

  • the code needs several steps or intermediate variables
  • you need to call an external service
  • the logic needs to be tested on its own
  • the transformation is central enough that it deserves a named, reusable unit

Common patterns

HTTP call with error handling

let http = import("requests")

let response = http.get(env.STATUS_API_URL, {
  headers: { "Authorization": "Bearer " + env.API_TOKEN }
})

let body = unwrap(response, { status: "unknown" })
{ service_status: body.status }

Conditional notification

let mail = import("email")

let amount = $.invoice.total
let recipient = if (amount > 10000) { env.ALERTS_EMAIL } else { env.INFO_EMAIL }

unwrap(mail.send({
  to: recipient,
  subject: "Invoice " + $.invoice.id,
  text_body: "Amount: $" + string.toString(amount)
}))

{ notified: true, to: recipient }

Data pipeline

let arr = import("array")
let str = import("string")

let raw = $.records

let cleaned = arr.filter(raw, fn(r) { r.email != null })

let normalized = arr.map(cleaned, fn(r) {
  {
    email: str.lowercase(r.email),
    name: str.trim(r.name),
    source: "import"
  }
})

let by_domain = arr.reduce(normalized, {}, fn(acc, r) {
  let domain = str.split(r.email, "@").1
  let existing = map.get(acc, domain, [])
  map.put(acc, domain, arr.concat(existing, [r]))
})

{ processed: arr.length(normalized), by_domain: by_domain }

Validation

The CLI validates script syntax with archastro configs validate, and the portal also validates syntax when you save. Syntax errors (mismatched braces, unknown operators, malformed expressions) are caught at validation time.

However, validation does not check runtime function availability. A script that calls a function that does not exist in the imported namespace will pass validation but fail at execution time. Always test scripts with sample input before deploying them in a live workflow.


Writing and testing scripts

Write scripts locally in your editor or coding agent, then deploy them as configs:

  1. Generate a sample with archastro configs sample script.
  2. Write the custom logic in your local file.
  3. Validate with archastro configs validate -k script -f ./path/to/script.yaml.
  4. Deploy with archastro configs deploy.

You can also validate and run scripts directly from the CLI:

archastro script validate -f ./path/to/script.yaml
archastro script run -f ./path/to/script.yaml --input '{"key": "value"}'
archastro script docs

archastro script docs prints the full script language reference.

The portal also provides a script editor with a built-in test runner:

  1. Open Scripts in the portal.
  2. Run a script with sample input to verify behavior.
  3. Use version history for rollback if a later change is wrong.

Good scripts are small enough to review quickly, narrow enough to explain in one sentence, easy to test with sample input, and focused on one job. When a script starts absorbing too much workflow logic, the visual process disappears and the workflow becomes a box of code -- a sign that the script should be split or the workflow restructured.


Debugging scripts

When a script fails, check these in order:

1. Check the routine or automation run

archastro list agentroutineruns --routine <routine_id>

The run list shows status and error messages for each execution.

2. Use println for inspection

println outputs values to the console panel in the portal script editor. Use it to inspect intermediate values:

let data = $.payload
println("received:", data)
let items = data.items || []
println("item count:", array.length(items))

3. Common errors and fixes

Error Cause Fix
unknown_function: env Calling env() as a function Use env.KEY (dot access, not function call)
unknown_function: http_post Using wrong function name Use import("requests") then http.post(...)
unknown_identifier: params Expecting implicit variables Use $ for input payload, env.KEY for env vars
cannot_access_property on array Using .length property Use array.length(items) (function, not property)
invalid_arguments: array.map Input is not an array (e.g. got a 404 JSON response) Check the HTTP response before mapping: if (resp.body.items) { ... }

4. Validation vs runtime

archastro configs validate checks syntax only. A script can pass validation but fail at runtime if:

  • an env var is not configured
  • an HTTP endpoint returns an unexpected response
  • a namespace function receives wrong argument types

Test scripts with sample input — either locally or in the portal editor — before deploying them in routines.


Further reading

See the Script Language Reference for the full specification, including all namespace functions, operator precedence, and error handling details.