Workflows#
A workflow is boxes joined into a graph: something starts it, its boxes run one after another or side by side, a condition picks a way, and it ends with outputs. Each time a workflow runs is an execution. This page explains how the boxes connect, what passes between them, and what makes an execution durable. Each kind of box is in Boxes; what starts a workflow is in Triggers.
flowchart LR
T([HTTP call]) --> R[read<br/>ask a model]
R --> C[category<br/>classify]
R --> B{big<br/>condition}
B -- over --> N[tell<br/>notify]
C --> N
Boxes and needs#
A box's needs names the boxes that must end before it starts; that is
what an arrow in the editor is. Boxes that need nothing of each other run
at the same time: above, category and big both start once read has
answered. The graph has no cycles, and has at most 64 boxes.
A box starts when every box it needs has succeeded. When one of them was skipped, it is skipped too, so what follows a way not taken does not run. When one of them failed, it is skipped, unless it is an error path.
Conditions and ways#
A box may have a when: an expression asked once, before it starts. When
it does not hold, the box is Skipped.
A condition box (switch) chooses one way among several: its cases
are asked in order, the first that holds is taken, and none holding takes
its default (else unless you name one). The way taken is its branch
output. The boxes after it say which way they belong to in their when:
- id: big
kind: switch
needs: [read]
cases:
- {name: over, when: "steps.read.outputs.amount >= inputs.limit"}
default: under
- id: tell
kind: notify
needs: [big]
when: steps.big.outputs.branch == 'over'
title: Over the limit
In the editor, connecting a box to a condition sets that when for you,
on the condition's next way not yet taken; click the arrow to choose
another. An if/else is a condition with one case.
Joins. A box after several ways needs them all, and one of them is
always skipped. Give it allow_skipped: true and it runs once the others
have ended — but never after a failure.
Error paths#
A box with runs: failure is an error path: it waits for the boxes it
needs to end and runs only when one of them failed (or was cancelled) — to
tell someone, or to clean up. When none failed, it is skipped. It runs even
as the execution stops, and the execution still fails: an error path
reports a failure, it does not hide it. runs: always runs once its needs
have ended, however they ended. Both need needs.
- id: work
kind: run
template: {image: python:3.12-slim, command: python, args: ["-c", "print('working')"]}
- id: tell-failure
kind: notify
needs: [work]
runs: failure
title: The job failed
message: "${{ steps.work.state }}: open the execution for its logs."
The palette's Error path box is a notify box with runs: failure.
When a box fails#
- Retries. A box that fails is tried again, as a new attempt: twice by
default (
retry: {max, backoff_seconds}, at most 10, the pause at most a day). Models, classifications, functions, runs, agents and sub-workflows are retried. An approval or a wait for an event is asked again only when a person retries the box; a notice and a condition never fail on their own and are not retried. - Time limits.
timeout_secondsbounds one attempt: 360 s by default for a model or a classification (a minute to answer, after up to five for a deployment at zero to wake), 60 s for a function, none for the rest. A whole execution has its owntimeout_seconds, 60 s to 30 days. - For good. A box that fails all its attempts fails the execution
(
on_failure: stop, the default): the boxes under way are cancelled and nothing new starts, except error paths. Withon_failure: continue, the boxes that do not depend on it go on, and the execution fails at the end. - By hand. Retry step on the execution's page gives a failed box one more attempt; the execution resumes from there.
repeat: {until, max} runs a box again until a condition holds (at most
20 times) — a poll, a loop that refines an answer.
What passes between boxes#
Outputs are small, named values. A model box's text, a classify
box's label and confidence, a function's result and each field it
returned, a condition's branch, a run's key=value lines. Boxes read
them in expressions: steps.<id>.outputs.<key>. A light box's outputs
are at most 16 KiB together; a run's, 4 KiB.
Expressions are a small, safe language: names (inputs.<name>,
event, steps.<id>.outputs.<key>, steps.<id>.state,
execution.name), literals, == != < <= > >=, && || ! and
contains(a, b). Text fields (a prompt, a title, a run's command,
arguments and environment) are templates: each ${{ … }} is replaced
by its value. A box may read only the boxes it depends on.
Reading into JSON. When a value is JSON — the event, a model's JSON
answer, what a function returned — more names read into it: event.amount,
event.card.last4, steps.read.outputs.json.items.0.name (an item of a
list by its index). A field that is not there is null. See
Expressions and templates.
Files go on the drive. Each execution has a drive mounted read-write at
/flow in every run and agent box. Name a shared drive of the workspace in
the workflow's drive and each execution gets its own folder of it — every
box, on any machine, sees what earlier boxes wrote. Without one, each
execution gets a drive of its own, deleted with the execution, which is a
copy per machine: boxes on different machines do not see each other's
files. Light boxes do not use the drive: what they pass is their outputs.
Durable#
- A machine going away loses nothing. Each box's state is kept by the cluster, not by a process. A run whose machine is lost is placed on another; boxes already finished never run again.
- Nothing runs twice by accident. Every run, approval or child an attempt makes is named after the execution, the box and the attempt, so a restart finds it instead of making another. A light box's call is made at least once: in the rare case its answer is lost while it runs, the question is asked again rather than lost (a function that must not run twice uses an idempotency key of its own).
- Versions. Each change writes a new, immutable version; an execution keeps the version it started with. The current version runs by default, and its triggers are the ones that fire.
An execution is Pending, Running, Waiting (only on people, events or
a sleep), Succeeded, Failed or Cancelled. Its page draws the graph
live, with each box's attempts, their runs and approvals, the outputs, the
inputs and event it started with, and the receipts of its agent runs.
Places#
concurrency: {max} limits how many executions of a workflow run at once
(1 to 100). When they are all taken, a new one waits its turn
(Pending, oldest first, its reason Queued: 2 of 2 running, 1 ahead), or,
with when_full: drop, is refused — an HTTP call gets 429
FLOW_BUSY, and a schedule refused so records why on its trigger.
Without concurrency, there is no limit.
Who it runs as#
An execution runs as a person, and what it does is done with that person's access in the workspace:
| Started by | Runs as |
|---|---|
A person (console, astra, API) |
That person. |
| An HTTP trigger | Whoever made the trigger's address — as long as they still reach the workspace and may start the workflow (flows:trigger). |
| A schedule | The workflow's owner: whoever created it. |
A flow box |
The parent execution's person. |
A model or classify box asks as that person: an assistant reads and keeps
their memory, and the tokens count as theirs. A notify box with no
to tells that person.
Everything a workflow names — deployments, assistants, functions, agents, drives, other workflows — is of its own workspace; a reference to another is refused when it is written.
What is kept#
An execution keeps, with the cluster: its inputs and its event (what an
HTTP call carried, at most 128 KiB), each box's state and reasons, and each
box's outputs — a run's key=value lines, and a light box's outputs,
which hold a model's answer, a classification's reason or what a
function returned. Everyone who may read the workflow's executions in the
workspace reads them. The prompts and answers of agent runs stay on their
machines, as for any agent run; files a
box writes stay on the execution's drive. A notify box's title and message
go to the people it tells (their inbox, phone and Telegram).
Finished executions are kept as the workspace's retention says
(runs_days); an execution's own drive is deleted with it, or 7 days after
its last use once it has ended.
Three ways to write one#
- The console's editor: drag boxes from the palette, connect them, fill each one in — or start from a recipe. The same workflow is on the YAML tab.
- YAML applied with
astra iris workflows apply -f. - Python, with the
astralyx.irisSDK.