Skip to content

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_seconds bounds 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 own timeout_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. With on_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.iris SDK.

See Build and run a workflow.