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(noconst,var, orfunctionkeywords) - 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_namedeclares 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.reduceinstead offororwhile
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:
- Generate a sample with
archastro configs sample script. - Write the custom logic in your local file.
- Validate with
archastro configs validate -k script -f ./path/to/script.yaml. - 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:
- Open Scripts in the portal.
- Run a script with sample input to verify behavior.
- 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.
Need something clearer?
Tell us where this page still falls short.
If a step is confusing, a diagram is misleading, or a workflow needs a better example, send feedback directly and we will tighten it.