Skip to content

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.

card-spending.yaml
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:

echo "verdict=approve" >> "$ASTRAEUS_OUTPUTS"
echo "score=0.93" >> "$ASTRAEUS_OUTPUTS"

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.limit compares 250 with 200 as numbers.
  • false, null, "", "false", 0 and "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.until may also name the box itself; the workflow's outputs may 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}.