Tasks and functions
An atom is written once and reached two ways. This page is the translation table between them — read it once and every reference entry in this section becomes predictable.
One atom, two adapters
Section titled “One atom, two adapters”Each atom is a self-contained unit: it takes parameters, does something, returns a result. It knows nothing about flows or scripts.
Two thin adapters sit on top of it:
- The task adapter makes it a node a flow can schedule. It reads parameters out of the flow’s YAML, runs the atom, and hands the result back to the run so later steps can use it.
- The function adapter makes it a callable inside a script. It reads parameters out of the argument you pass, runs the atom, and returns the result to your code.
shell atom (parameters, work, result) ▲ ▲ task adapter function adapter │ │ shell: in a flow ─┘ └─ shell.execute() in a scriptThe practical consequence: you cannot get a behaviour difference between the two surfaces,
because there is only one behaviour. If archive handles a .tar.zst in a flow, it handles it in
a script. If a database query returns rows in one shape here, it returns that shape there. When
the reference documents a default, that default is the atom’s, and it holds on both sides.
What does differ is the calling convention — the spelling, the shape of the arguments, and how you get the result back out. That is the rest of this page.
Naming
Section titled “Naming”| Flow | Script | |
|---|---|---|
| Reached as | a task key | a namespace |
| Spelled | shell: | shell.execute() |
| Granularity | one key, mode chosen by parameters | one function per verb |
A flow task is a single key, and where an atom does several things the mode is a parameter. A namespace splits the same atom into named functions:
tasks: - name: fetch-release s3: command: download bucketname: releases s3url: "{{minio.url}}" credentials: accesskey: "{{minio.username}}" secretkey: "{{minio.password}}" secure: false commandparams: object: build.tar.gz path: ./build.tar.gz// minioUrl, minioUser and minioPassword arrive as bare globals,// from --vars or an --env file.function main() { return s3.download({ endpoint: minioUrl, accessKey: minioUser, secretKey: minioPassword, bucket: 'releases', object: 'build.tar.gz', path: './build.tar.gz', });}The verb moves from a parameter (command: download) into the function name (s3.download). That
is the single most common difference you will meet, and once you expect it, most translations are
mechanical.
Parameter names are per-surface
Section titled “Parameter names are per-surface”Look at those two tabs again. They call the same atom with the same values, and almost none of the keys match:
| Flow | Script | |
|---|---|---|
| Bucket | bucketname | bucket |
| Server | s3url | endpoint |
| Credentials | nested under credentials: | flat, alongside the rest |
| Operands | nested under commandparams: | flat, alongside the rest |
Do not assume a parameter name carries across. The two adapters were written for two very different calling conventions — a flow’s parameters are a YAML tree an operator reads, a script’s are one object you build in code — and they are named for their own surface.
Two patterns are worth keeping in your head, because they cover most of it:
- Case. Where a name is shared, a flow spells it lowercase and a script spells it camelCase:
workingdir/workingDir,inheritenv/inheritEnv. YAML keys are matched case-sensitively, soworkingDir:in a flow is not the parameter you meant — it is an unrecognised key, and the operation quietly uses its default. If a flow parameter appears to do nothing, check its case first. - Nesting. Flows group credentials and operands into sub-blocks; scripts pass one flat object.
Every reference entry lists both surfaces in full, so you never have to translate from memory.
Getting the result back
Section titled “Getting the result back”This is where the two surfaces genuinely diverge, because a flow and a script hold state differently.
A flow has a shared variable space for the whole run. An atom writes into it with setvar,
and later steps read it back with {{ }} templating:
name: releaselist: truetasks: - name: get-version shell: script: git describe --tags setvar: version
- name: announce print: message: "Releasing {{version}}"The value outlives the step. Anything after it in the run can read {{version}}.
A script has ordinary variables. The call returns its result and you assign it:
function main() { const v = shell.execute({ script: 'git describe --tags', capture: true }); log.info(`Releasing ${v.output}`); return { version: v.output };}Note capture: true — a script returns output to your code only when you ask it to, because the
default is to stream straight to the terminal.
So: setvar in a flow, a return value in a script. A flow’s variables are the run’s memory; a
script’s variables are the function’s.
The result shape
Section titled “The result shape”Namespace functions return a map with a consistent envelope:
| Field | Meaning |
|---|---|
success | Whether the atom completed |
error | The message when it did not; empty when it did |
| (operation fields) | Whatever this atom produces — output, rows, path, … |
Check success rather than assuming; a failed atom returns a value, it does not throw:
function main() { const r = shell.execute({ script: './deploy.sh', capture: true }); if (!r.success) { return { ok: false, reason: r.error }; } return { ok: true, log: r.output };}In a flow the equivalent check is structural rather than written by hand — a failed task fails the node, and where the run goes next is decided by the flow’s error routing. See Error handling.
How to read a reference entry
Section titled “How to read a reference entry”Every entry in the atom reference follows one shape:
- What it does — one paragraph.
- Parameters — a table with both spellings, types, defaults and whether each is required.
- Both surfaces — a Flow tab and a Script tab, doing the same thing.
- Result — the fields you get back.
- Notes — anything that will bite you.
Pick the tab that matches what you are writing. Your choice is remembered across the whole site, so if you work in scripts you see script examples everywhere until you switch.
When there is only one surface
Section titled “When there is only one surface”Most operations have both forms, but not all, and a few have a narrower function form than task form.
- Some atoms exist only as tasks, because they mean nothing outside a run —
suspendpauses a flow until it is resumed,throwfails a node deliberately,printwrites into the run log. There is nothing for a script to call; a script already hasreturn,throwandlog.info. - Some exist as both, but the namespace covers less than the task.
waitis the clearest case: as a task it polls several kinds of condition, as a namespace it offerswait.httponly. - Some capabilities exist only as namespaces, because they are language services rather than
units of work —
log,json,string,math,array,time, and the authorization namespaces a script uses to ask who the caller is.
The reference marks all three cases. Where a tab would be empty, it says so and says what to use instead.
See also
Section titled “See also”- Flows and scripts — choosing between the two models
- Libraries — which atoms exist, and where
- Atom reference — every atom, both surfaces