Workflow specification#
A workflow file is the workflow in YAML (or JSON). In the API, the YAML and
the CLI the resource is called Flow (kind: Flow, apiVersion:
anemoi.astralyx/v1, paths /anemoi/flows and /anemoi/flow-executions);
its executions are flow executions, and its boxes are steps.
astra iris workflows apply -f and the console's YAML tab read the
file; the API takes {metadata, spec}. Every version is immutable once
written.
apiVersion: anemoi.astralyx/v1
kind: Flow
metadata:
name: card-spending
spec:
description: Reads each card purchase, files it, tells me when it is over my limit.
inputs:
- {name: limit, description: Tell me about purchases from this amount, default: "200"}
triggers:
- {id: bank, kind: http, dedupe: "${{ event.id }}", dedupe_seconds: 600}
concurrency: {max: 2}
steps:
- id: read
kind: model
assistant: money
format: json
prompt: |-
Read this card purchase notice and answer with JSON: amount (a number), merchant, date (YYYY-MM-DD).
${{ event.text }}
- id: category
kind: classify
needs: [read]
assistant: money
input: "${{ steps.read.outputs.merchant }}"
labels:
- {name: food, description: "groceries, restaurants, bakeries"}
- {name: transport, description: "fuel, rides, parking, transit"}
- {name: other, description: anything else}
- id: big
kind: switch
needs: [read]
cases:
- {name: over, when: "steps.read.outputs.amount >= inputs.limit"}
default: under
- id: tell
kind: notify
needs: [big, category]
when: steps.big.outputs.branch == 'over'
title: "R$ ${{ steps.read.outputs.amount }} at ${{ steps.read.outputs.merchant }}"
message: "Category: ${{ steps.category.outputs.label }}. Over your limit of R$ ${{ inputs.limit }}."
outputs:
amount: "${{ steps.read.outputs.amount }}"
merchant: "${{ steps.read.outputs.merchant }}"
category: "${{ steps.category.outputs.label }}"
The document#
| Field | Description |
|---|---|
apiVersion |
anemoi.astralyx/v1 (may be left out). |
kind |
Flow (may be left out). |
metadata.name |
Required: the workflow's name in the workspace, [a-z][a-z0-9_-]*, at most 40, not ending in - (the console takes letters, digits and -). |
metadata.labels |
Optional labels. |
spec |
Below. |
spec#
| Field | Type | Default | Description |
|---|---|---|---|
description |
string | — | For people; at most 4 096 bytes. |
triggers |
list | — | What starts it on its own: at most 10 (Triggers). Without any, it runs only when someone runs it. The current version's triggers are the ones that fire. |
inputs |
list | — | [{name, description, required, default}]. name: [a-z0-9_], at most 64; default at most 64 KiB. Inputs are text: ${{ inputs.<name> }}. An input required with a default is never missing (a warning). Do not declare one named event: that name is the event. |
steps |
list | required | The boxes: 1 to 64; their needs make a graph without cycles. |
outputs |
map | — | name → template, computed when the execution succeeds. Names [a-z0-9_], at most 64. Any box may be read. |
timeout_seconds |
integer | none | The whole execution's limit: 60 to 2 592 000 (30 days). |
on_failure |
string | stop |
stop: a box failing for good fails the execution and cancels the boxes under way (error paths still run). continue: boxes that do not depend on it go on; the execution fails at the end. |
drive |
string | — | A drive of the workspace (a shared one, for boxes on several machines): each execution mounts its own folder of it at /flow. Without one, each execution gets a drive of its own, a copy per machine, deleted with it. |
concurrency |
object | none | {max, when_full}: at most max (1 to 100) executions at once; when_full is queue (the default: a new one waits, Pending, oldest first) or drop (a new one is refused: 429 FLOW_BUSY). Without it, no limit. |
Every box#
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | [a-z][a-z0-9_-]*, at most 40, not ending in -, unique. |
kind |
string | required | model, classify, function, notify, switch, run, agent, flow, approval, wait_event, sleep. |
description |
string | — | For people; at most 4 096 bytes. |
needs |
ids | — | Boxes that must end first. Without it, the box starts at once. |
when |
expression | — | Run only when it holds; otherwise the box is Skipped. Asked once, before the first attempt. |
allow_skipped |
boolean | false |
Run even when a box it needs was skipped (a join after a condition's ways). Without it, a box whose need was skipped is skipped too. A need that failed still skips it. |
runs |
string | success |
success: after its needs succeeded. failure: only when one of them failed or was cancelled — an error path; it runs even as the execution stops, and the execution still fails. always: once its needs have ended, however. failure and always need needs, and are not repeated. |
retry |
object | {max: 2} |
max: attempts after the first, at most 10; backoff_seconds: wait before the next, at most 86 400. Applies to model, classify, function, run, agent and flow. approval and wait_event are asked again only when a person retries the box; notify and switch are never retried. |
repeat |
object | — | until (an expression, which may read the box itself) and max (1 to 20): run the box again, as new attempts, until it holds. Its retries count afresh in each iteration. |
timeout_seconds |
integer | by kind | One attempt's limit: 1 to 2 592 000. Default: 360 for model and classify, 60 for function, none for the rest. For wait_event, the wait's. |
Each kind#
Light boxes#
Run in seconds, without a container: a call to what is already up.
| Kind | Fields | Outputs |
|---|---|---|
model |
deployment or assistant (one of them, of the workspace); prompt (template, required); system (template); images (templates, at most 8, each a data:image/…;base64, URL); format (text, the default, or json); max_tokens (1 to 32 768; the model's own when absent); thinking (false by default: a model that reasons first answers at once) |
text (cut at 16 KiB). With format: json: json (the object, at most 8 KiB) and each top-level field whose name is [a-z0-9_], at most 64. An answer that is not a JSON object fails the attempt. |
classify |
deployment or assistant; input (template, required); labels (2 to 20: {name, description}, name [a-z0-9_-] at most 63, not uncertain); min_confidence (0 to 1, default 0.6); images (as model's) |
label (one of the labels, or uncertain below min_confidence or when the model names no label offered), confidence (0 to 1, two decimals), confident (true/false), reason (at most 1 KiB), guess (the model's label, when not confident) |
function |
function (required, of the workspace); target (an alias, v<n> or latest, the default; draft is refused); input (field → template; a field's name at most 64) |
result (what it returned, as JSON, cut at 8 KiB) and, for an object, each top-level field |
notify |
title (template, required; cut at 300 characters); message (template; cut at 2 000 characters); to (user:<id>; the person the execution runs as when empty); channels (app, telegram, email; all the person keeps when empty) |
sent (true), to (who, comma-separated) |
Control boxes#
| Kind | Fields | Done when | Outputs |
|---|---|---|---|
switch |
cases (1 to 20: {name, when}, name [a-z0-9_-] at most 63, when an expression); default (the way when none holds; else when empty) |
At once: the first case whose when holds is taken, else default. |
branch |
approval |
target (template: what approvers read); approvers (role:admin, role:editor, role:viewer, user:<id>; editors when empty); wait_seconds (60 to 604 800; 86 400 by default) |
Someone approves. Denied, or not decided in time, it fails. | decision (approved), decided_by |
wait_event |
name ([a-z0-9_-], at most 63) |
The event arrives (POST …/flow-executions/{e}/events/{name}); one sent before the box waits counts. Without timeout_seconds, it waits as long as the execution may (a warning). |
The event's data |
sleep |
seconds (1 to 604 800) |
The time has passed. | — |
Cluster boxes#
| Kind | Fields | Done when | Outputs |
|---|---|---|---|
run |
template: one worker (image, command, args, env, requested_resources, drives…, as in the run specification); command, args and env values are templates. Without resources, 1 CPU core and 1 GiB. |
The run ends. Its restart policy is Never: retry retries. It may not mount anything at /flow. |
What it writes to $ASTRAEUS_OUTPUTS |
agent |
agent (required), version (from 1; the current one when absent), input (template, required) |
The agent's run ends; its answer is written to /flow/<box>/answer.md. Budgets, guardrails, approvals of its tool calls and receipts apply as to any agent run. |
What its run writes to $ASTRAEUS_OUTPUTS |
flow |
flow (required, not itself), version, inputs (name → template) |
The child execution ends. At most three workflows deep. | The child's outputs |
Outputs of a run#
A run box (or an agent's run) writes its outputs to the file named by
$ASTRAEUS_OUTPUTS, one key=value per line:
Keys are lowercase letters, digits and _, at most 64; at most 64 pairs,
4 KiB in all. They are reported when the run ends.
Triggers#
Every trigger has id ([a-z][a-z0-9_-]*, at most 40, unique),
description and kind.
kind: http#
| Field | Type | Default | Description |
|---|---|---|---|
mode |
string | async |
async: the call is answered 202 at once, with the execution. sync: the call waits for the execution to end and is answered with its outputs (the workflow as an API). |
wait_seconds |
integer | 60 | sync only: how long a call waits, 1 to 90. Past it, the call is answered 202, as async. |
auth |
string | url |
How a call proves it may: url (the secret is the address's last part), bearer (Authorization: Bearer <secret>), hmac (an HMAC-SHA256 of the body with the secret, in signature_header). |
signature_header |
string | X-Signature-256 |
hmac only: the header holding the signature, sha256=<hex> or <hex>. Letters, digits and -, at most 64. |
per_minute |
integer | 60 | Calls a minute, 1 to 600. |
dedupe |
template | — | A key read from the event (${{ event.id }}): calls with the same key within dedupe_seconds start one execution, and the later ones are answered with it. A key that renders empty is not remembered. It may read event and execution.name only. |
dedupe_seconds |
integer | 60 | How long a key is remembered: 1 to 86 400. |
allow_get |
boolean | false |
Also answer GET, its query as the event (a link someone opens). |
The address itself is made in the console or with the API: see Start a workflow with an HTTP call.
kind: schedule#
| Field | Type | Default | Description |
|---|---|---|---|
every_minutes |
integer | — | Every this many minutes, 5 to 1 440, at the multiples of the interval counted from 00:00 UTC (every 15: at :00, :15, :30, :45). |
at |
string | — | Or at this time of day, HH:MM. One of every_minutes and at, not both. |
days |
list | every day | With at: the days, 0 (Sunday) to 6 (Saturday). |
timezone |
string | UTC |
With at: an IANA time zone (America/Sao_Paulo, Europe/Lisbon). A time that does not exist on a day (a clock moved forward) is skipped that day; one that happens twice fires the first time. |
A schedule runs as the workflow's owner. See Run a workflow on a schedule.
The event#
Whatever started an execution is its event, read as event:
| Started by | event |
|---|---|
| An HTTP trigger | The call's body: JSON as it is; a form (application/x-www-form-urlencoded) as an object of its fields; other text as {"text": "<the body>"} (or as JSON, when the text is a JSON object or list); a GET, or an empty body, as an object of the query's fields. |
| A schedule | {"trigger": "<id>", "scheduled_at": "<RFC 3339 time, UTC>"}: the time it fired for. |
| A person, the CLI, the API | The event they give (any JSON), or nothing. |
At most 128 KiB, as text. An event that is text stays text: event is
the text, and event.<field> is null.
Expressions and templates#
A template is text with ${{ expression }} in it, each replaced by the
expression's value as text. when, repeat.until and a switch's
cases[].when are expressions, written bare or inside ${{ }}. At most
16 KiB each.
| Element | Forms |
|---|---|
| Names | inputs.<name>, event, event.<field>, steps.<id>.outputs.<key>, steps.<id>.state (Succeeded, Failed, Skipped, …), execution.name |
| Inside JSON | After event, inputs.<name> or steps.<id>.outputs.<key>, more names read into a JSON value: a field of an object (event.card.last4), an item of a list by its index (steps.read.outputs.items.0.name). |
| Literals | 'text' or "text", numbers, true, false, null |
| Operators | ==, !=, <, <=, >, >=, &&, \|\|, !, parentheses |
| Functions | contains(a, b): whether text a contains b |
- A comparison is numeric when both sides read as numbers, textual
otherwise:
steps.read.outputs.amount >= inputs.limitcompares250with200as numbers. false,null,"","false",0and"0"are false.- An output a box did not write, a field that is not there, an index past
the end, or a name read into a value that is not JSON, is
null(empty in a template). - Read into JSON, a number is a number and a boolean a boolean; an object
or a list is its JSON text (
contains(event.tags, 'food')looks in it). - A box may name only boxes it depends on (through
needs, directly or not);repeat.untilmay also name the box itself; the workflow'soutputsmay name any box. Names that cannot exist are refused when the workflow is written.
Nothing is evaluated as code.
The execution#
GET $ASTRALYX_API/anemoi/flow-executions/{name}:
| Field | Description |
|---|---|
metadata.name |
<workflow>-<6 hex>. |
spec |
flow, version, inputs (defaults filled in, and event), started_by, owner (who made the workflow), trigger ({id, kind}, when a trigger started it), flow_spec (the version as it was), drive, drive_path, parent (a child's), depth. |
status |
state: Pending (not started, or queued for a place), Running, Waiting (only on people, events or a sleep), Succeeded, Failed, Cancelled; reason; times. |
steps |
<id>: {state, reason, attempts: [{n, name, state, reason, node, started_at, finished_at, outputs}], outputs}. A box is Pending, Running, Waiting, Succeeded, Failed, Skipped or Cancelled. An attempt's name is the run, approval, child execution or event it made; a light box's outputs are kept on the box, not on its attempts. |
outputs |
The execution's outputs, once it succeeded. |
events |
[{name, data, sent_by, at}]: the events sent to its wait_event boxes. |
receipts |
[{step, attempt, agent, run}]: its agent runs' receipts. |
Starting an execution answers it, with deduplicated: true when an
HTTP trigger's call found the execution an earlier call with the same
key started. Listings (GET $ASTRALYX_API/anemoi/flow-executions?flow=&state=)
give {metadata, flow, version, started_by, status, step_counts}.