Templates
{{ }} resolves against the run context. Registered helpers work anywhere a template does,
so conditional prompt text is an ordinary {{#if}}:
eq, contains, filter and toJSON.
There is no {{input.*}} root. A wrong path resolves to empty, silently, which is the
single most common reason a call arrives with a field missing. Array elements and object
keys are dot segments, never brackets: records.0.Name.
prompt.<blockName> is why a manifest never hard-codes instruction text. Blocks live in
prompts/blocks/**/*.md, are toggleable, and are camelCased from the filename, so
markdown-guidelines.md becomes {{prompt.markdownGuidelines}}. A block’s words copied
into a node is a fork that stops tracking the block.
Expressions
A string starting withreturn is a sandboxed expression, evaluated at any depth. Reach
for one when a value’s shape depends on the run: an array member that is only sometimes
present, or a key whose name varies by model.
Security is by absence. There is no process, require, fetch, eval, new,
assignment or constructor to reach, because the interpreter never implements them.
Available: member access, indexing, literals, spread, template strings, operators,
ternaries, arrow callbacks, and JSON, Math, Number, String, Boolean, parseInt,
parseFloat, encodeURIComponent, Object.* and Array.*. The platform adds three more,
because without them a node could not be a manifest at all:
Date.now()andDate.iso(ms). Half the APIs a node calls take a date range and want an ISO string, soDate.iso(Date.now() - 30 * 86400000).split('T')[0]is thirty days ago asYYYY-MM-DD. There is nonew Date(...).sha256(value). A stable id derived from content, which downstream dedup joins on.toBase64(value). Plain text as UTF-8 bytes in base64, the form bytes take on the way to anencoding: binarybody.
.at(-1) rather than .pop(), and .toSorted() rather than
.sort(). The array you would be sorting is a live upstream output, so sorting it in place
reorders it for every other node reading the same value.
Named helpers
An expression is one string, which is fine forreturn response.data and bad for a row
projection. When one grows past a few lines, declare it as a helper in any shared/*.yaml
file. Helpers are collected across the package and callable from every expression in it:
shared/helpers.yaml
api/service.yaml
config, credentials or
the scope of whatever called it. Same sandbox, no extra authority, and a broken body fails
when the package loads rather than on the first request that reaches it.
Declare helpers next to the call they shape rather than in one large file. Two files
declaring the same helper name is an error rather than a merge.
The run context
What your calls can see, and where each piece comes from.user is identity, and nothing that authenticates as them. Email, id and name, never
the caller’s access token. A token is the user against our own services, so a node holding
one could send it to any host its package allows, while an email authenticates nothing.
It is a first-class root because “who is asking” is the join key for every CRM, support and
account node. Reading identity out of the request instead would let a caller fetch somebody
else’s record.
calls.<name> is how a second call uses the first. A call skipped by its when leaves
no key at all, so calls.x is also how you ask whether it ran.
Workflow-level values
Set on the workflow and shared by every node in it:workflow.variables, plus
workflow.id, workflow.name, workflow.runId, workflow.userId and
workflow.conversationId. Inside a loop, loop carries the current item, and saved
carries outputs other nodes chose to keep.
These resolve before your node runs, while its settings are being prepared, so they
belong in a config.yaml field and your calls read the result as {{ config.<field> }}:
config.yaml
Next steps
Config Schema
The settings form these values resolve into.
api/run.yaml
Every field a call takes, generated from the schema.

