Actions
■ An action is an executable file in actions/ or any subfolder of it, in
any language its first line names (#!/bin/sh, #!/usr/bin/env python3).
It has no extension and no description file; its name is its file name,
unique among the actions (actions/mail/imap-check is #imap-check).
Files that are not executable (a README.md, a package.json) can sit
beside actions; they are not actions.
actions/ship:
#!/bin/sh# hands $order to the carriercurl -fsS "https://carrier.example.com/ship?order=$order" | jq '{tracking: .id}'What it gets
Section titled “What it gets”- Its values, as environment variables:
$order,$host. A value is the first found in the step’s data, the action’s args, then the run’s state (Args and secrets). A value that is not text arrives as JSON text. A name that cannot be an environment variable, and a state value over 64 KB (a whole web page, say), are on stdin only: the system would refuse to start the action with them. A value that big in a step’s data or args is an error naming it. - The run’s whole state, as JSON on stdin.
$ITERO_MEMORY: a file it may read and write, kept between runs, one per workflow (for “the last mail seen”).$ITERO_WAITING_SINCE, when it runs again after waiting.
It runs in the workspace, with the daemon’s own itero first on its
PATH, so an action can send a message with itero emit.
What it says
Section titled “What it says”It prints one JSON object, whose fields join the run’s state; a field set
to null is removed. Printing nothing adds nothing. Its exit code:
| Exit | Means |
|---|---|
0 |
done |
75 |
not yet: what it printed joins the state, and the run waits; the step runs again when a matching message comes, or every hour |
| anything else | an error: the end of its stderr says why |
A step may print nothing for 10 minutes, quiet in
limits. Anything it prints, on stdout
or stderr, starts the 10 minutes again, so a long step that tells how it
goes (creating the cluster... on stderr) runs as long as it needs, up to
most (a day). Past either, or when the daemon stops while it runs, it
runs again, up to 3 times; then it is an error, quiet for 10m or
ran for 1d.
Versions
Section titled “Versions”A version is part of the file name: notify@0.2, notify@0.10. #notify
is the highest, compared as numbers; #notify@0.2 pins one. Args and
secrets are the action’s, whatever the version.
Built in
Section titled “Built in”#notify shows a desktop notification: through Itero.app on macOS
(osascript without it), notify-send on Linux.
| Value | Is |
|---|---|
$text |
what it says; needed |
$title |
its title; itero when not set |
$subtitle |
a line under the title (on Linux, the text’s first line) |
$sound |
default, or a macOS sound’s name such as Glass (those in /System/Library/Sounds); on Linux, a sound theme’s name |
$url |
opened by a click on it, on macOS through Itero.app: http or https, anything else an error, as it may come from a message |
$buttons |
a list of 1 to 4 labels, shown as its buttons (macOS); the step then waits for an answer |
$wait |
true: the step waits for a click on it, with no buttons |
$data |
an object a click on it passes on, as data |
{ "#notify": { "title": "Backup", "subtitle": "disk 2", "text": "$why", "sound": "Basso" } }On macOS, each click on a notification from #notify is a message,
itero.notification.clicked, in the workspace that showed it: answer is
the button’s label, open for a click on the notification itself, or
dismissed when one that waits is closed without one. A workflow can start
on it (Messages from the daemon),
and a step with $buttons or $wait waits for it, then the run goes on
with $answer in its state:
{ "steps": [ "#plan", { "#notify": { "title": "Deploy?", "text": "$summary", "buttons": ["Deploy", "Cancel"] } }, { "#apply": { "on": [{ "$answer": "Deploy" }] } }] }A run nobody answers waits until its workflow’s timeout. On Linux a
notification has no buttons and tells no clicks.
How long a notification stays is yours to set, in System Settings, Notifications, itero: Banners go away by themselves, Alerts stay until dismissed.
#forget forgets ended runs and their messages past keep, and
#roll-log rolls the log; the built-in workflows use them
(The daemon’s own workflows).
A workspace action of the same name is used instead of a built-in one.
Other languages, and their dependencies
Section titled “Other languages, and their dependencies”The first line picks the language. A Python action:
#!/usr/bin/env python3import json, os, sysstate = json.load(sys.stdin)print(json.dumps({"total": state["price"] * state["count"]}))An action that uses packages should bring them, so it runs the same on any machine with its tool, and nobody installs anything by hand. itero installs nothing itself; each language’s tool does it:
Python with uv
Section titled “Python with uv”uv runs a script whose dependencies are written at its top (PEP 723, a Python standard). It installs them on the first run, in a cache of its own:
#!/usr/bin/env -S uv run --script# /// script# requires-python = ">=3.10"# dependencies = ["python-slugify>=8"]# ///import json, osfrom slugify import slugifyprint(json.dumps({"slug": slugify(os.environ["title"])}))examples/languages has this action (python/slugify) and another
(python/parse-date, with dateparser), each with a workflow around it,
beside the Node, Bun and Docker ones below.
JavaScript with Bun
Section titled “JavaScript with Bun”Bun installs a package an action imports, at the version
the import names, with no package.json:
#!/usr/bin/env -S bun --install=forceimport { z } from "zod@3.23.8";--install=force matters: Bun turns this off when a node_modules folder
sits in any folder above the action, and one in your home folder is enough.
examples/languages’s bun/check-order validates an order with zod; a bad
one ends the run in error, its why listing the fields.
Node, and anything with a project folder
Section titled “Node, and anything with a project folder”A program with its own package.json, pyproject.toml or build keeps its
project in a folder, and the action starts it:
actions/node/links #!/bin/sh cd "$(dirname "$0")/links-app" && exec node main.jsactions/node/links-app/ package.json, package-lock.json, node_modules/, main.jsThat is examples/languages’s node/links: a page’s title and links, with
cheerio.
Only executable files are actions. The rest of the folder (package.json,
main.js, a README.md) is left alone, and node_modules and hidden folders
(.venv) are never looked into. Install it once yourself (npm install),
in that folder. itero starts the action in the workspace; this one moves
into its project before it starts the program.
Docker
Section titled “Docker”A program that comes as an image runs in it, with Docker pulling it the
first time. examples/languages’s docker/to-markdown turns a page’s HTML
into Markdown with pandoc, reading the page from the state on stdin:
#!/bin/shjq -r '.page' | docker run --rm -i pandoc/minimal:3.11.0 -f html -t gfm-raw_html --wrap=none | jq -Rs '{page: .[0:20000]}'Pin the image’s version, as here, so a new release does not change what
the action does. A value goes into the container as -e name (it takes
the value from the action’s environment, never from the command line) or
on stdin.
When something it needs is missing
Section titled “When something it needs is missing”itero finds what an action needs from the action itself, with no list to keep up to date:
-
When a step fails, it reads the exit code and stderr, and the run’s
why(initero runs <name>anditero errors) starts with what was missing:The action printed The run’s why exit 127, docker: not found(sh, bash, zsh, env)needs docker: not found on PATHModuleNotFoundError: No module named 'requests'needs the Python module requestsCannot find module 'axios'(Node)needs the module axiosCannot find package 'axios'(Node, Bun)needs the package axiosa first line naming a program that is not there needs /opt/python3 (the first line of …): not found -
Before anything runs,
itero checklists, under each action, what its first line needs and is not on thePATHactions run with (needs bun: not found on PATH). For a shell script it also reads the commands it runs, and lists the missing ones asmay need: a hint, since a script can build a command at run time or guard one withcommand -v.
Versions (python >= 3.11) and whether a service is up (Docker’s daemon, a
model server) are left to the action: it checks them itself, and exits 75 to
try again later.