Build and run a workflow#
This page writes a workflow four ways — the console's box editor, YAML with
astra, Python, the API — then runs it, follows its executions and changes
it. What the boxes are is in Boxes; every field is
in the Workflow specification.
Before you begin#
- The editor role in the workspace (
flows:create,flows:update,flows:trigger); viewers watch executions. - What the workflow names, in the workspace: here, an
assistant (or a deployment) that is up.
Below,
<assistant>stands for its name. - For the CLI:
astra loginandastra use <org>/<workspace>(see Install the CLI). - For the API:
ASTRALYX_APIandASTRALYX_TOKENas in the API Reference.
The example#
message-triage takes a message sent to its address, has a model summarise
it and classify its urgency, and tells you at once when it is urgent — or
asks you when the model is not sure. If a model box fails, an error path
tells you that too.
flowchart LR
T([inbox<br/>HTTP call]) --> S[summary<br/>ask a model]
T --> U[urgency<br/>classify]
U --> R{route}
R -- now --> N[tell]
R -- ask --> Q[unsure]
S --> N
S --> Q
S -. fails .-> E[on-error]
U -. fails .-> E
apiVersion: anemoi.astralyx/v1
kind: Flow
metadata:
name: message-triage
spec:
description: Reads a message, judges its urgency, tells me at once when it is urgent.
triggers:
- {id: inbox, kind: http, dedupe: "${{ event.id }}", dedupe_seconds: 600}
steps:
- id: summary
kind: model
assistant: <assistant>
prompt: "Summarise this message in one sentence:\n\n${{ event.text }}"
- id: urgency
kind: classify
assistant: <assistant>
input: "${{ event.text }}"
labels:
- {name: low, description: can wait days}
- {name: normal, description: today}
- {name: urgent, description: now}
- id: route
kind: switch
needs: [urgency]
cases:
- {name: now, when: "steps.urgency.outputs.label == 'urgent'"}
- {name: ask, when: "steps.urgency.outputs.label == 'uncertain'"}
default: later
- id: tell
kind: notify
needs: [route, summary]
when: steps.route.outputs.branch == 'now'
title: "Urgent: ${{ steps.summary.outputs.text }}"
message: "${{ steps.urgency.outputs.reason }}"
- id: unsure
kind: notify
needs: [route, summary]
when: steps.route.outputs.branch == 'ask'
title: "Not sure how urgent: ${{ steps.summary.outputs.text }}"
message: "The model guessed ${{ steps.urgency.outputs.guess }} (${{ steps.urgency.outputs.confidence }})."
- id: on-error
kind: notify
needs: [summary, urgency]
runs: failure
title: message-triage could not read a message
message: "Open ${{ execution.name }} to see why."
outputs:
urgency: "${{ steps.urgency.outputs.label }}"
summary: "${{ steps.summary.outputs.text }}"
What to notice: summary and urgency need nothing, so they run side by
side. route is a condition; tell and unsure each take one of its
ways, and a message of low or normal urgency takes later, where no box
is: both are skipped, and the execution succeeds. on-error runs only when
summary or urgency fails for good — and the execution still fails.
Write the workflow#
- Open Iris → Workflows and press New workflow (or, under Start from a recipe, Use it on one).
- Name:
message-triage. - On Boxes, the palette groups the boxes: Start (HTTP call, Schedule), Light (Ask a model, Classify, Function, Notify), Control (Condition, Error path, Approval, Wait, Wait for event) and Cluster (Run, Agent, Sub-workflow). Drag a box onto the graph: dropped on a box, it comes after that box; dropped elsewhere, it stands alone. A click adds it after the selected box.
- Connect boxes: drag from a box's dot to the next box (or click
the dot, then the box). From a condition, the connection takes the
condition's next free way — the box's Only when is set to
steps.route.outputs.branch == '<way>'for you. Click an arrow to choose its way (Along the way), make it an error path (Runs: When it fails), or Remove the connection. - Click a box to fill it in on the right: Who answers, the prompt, the labels, the ways, a Recipe to start from; then Needs, Only when, Runs and Retries, time limit, repeat, common to every box.
- Inputs, outputs & limits holds the workflow's inputs, its outputs, When a step fails (stop or continue), its time limit, its drive, and At most at once.
- YAML shows the same workflow as a file: an edit on either side
shows on the other. You can paste the file above there (and replace
<assistant>) instead of steps 3 to 6. - Under the editor, the checks — the console's own, then the cluster's — list problems as you go; click one to open its box. Press Create the workflow.
$ astra iris workflows validate -f message-triage.yaml
message-triage.yaml: flow message-triage is valid
$ astra iris workflows apply -f message-triage.yaml
flow message-triage: version 1 written, now current
validate runs the cluster's own checks on the file, here, and prints
their warnings; --remote also sends it to the cluster's check.
Neither looks for what the workflow names (the assistant, agents,
functions, drives): one that does not exist fails its box when it
runs. apply creates the workflow, or writes its next version
(--no-current keeps the current one). A file may also be JSON.
The astralyx package (Python 3.9 or newer, standard library only).
It is not published on PyPI yet.
from astralyx.iris import Client, Workflow
ASSISTANT = "<assistant>"
wf = Workflow("message-triage", description="Reads a message, judges its urgency, tells me at once when it is urgent.")
wf.http_trigger("inbox", dedupe="${{ event.id }}", dedupe_seconds=600)
summary = wf.model("summary", assistant=ASSISTANT, prompt="Summarise this message in one sentence:\n\n${{ event.text }}")
urgency = wf.classify("urgency", assistant=ASSISTANT, input="${{ event.text }}",
labels={"low": "can wait days", "normal": "today", "urgent": "now"})
route = wf.switch("route", needs=[urgency], default="later",
cases={"now": f"{urgency.outputs.label} == 'urgent'", "ask": f"{urgency.outputs.label} == 'uncertain'"})
wf.notify("tell", needs=[route, summary], when=f"{route.outputs.branch} == 'now'",
title=f"Urgent: {summary.output('text')}", message=urgency.output("reason"))
wf.notify("unsure", needs=[route, summary], when=f"{route.outputs.branch} == 'ask'",
title=f"Not sure how urgent: {summary.output('text')}",
message=f"The model guessed {urgency.output('guess')} ({urgency.output('confidence')}).")
wf.notify("on-error", needs=[summary, urgency], runs="failure",
title="message-triage could not read a message", message="Open ${{ execution.name }} to see why.")
wf.output("urgency", urgency.output("label"))
wf.output("summary", summary.output("text"))
assert wf.validate() == [], wf.validate()
c = Client.from_config() # what `astra login` and `astra use` saved
print(c.flows.check(wf)) # the cluster's checks: {ok, errors, warnings}
c.flows.apply(wf) # a new workflow, or its next version
step.output(key) is ${{ steps.<id>.outputs.<key> }}, for
templates; step.outputs.<key> and step.state are the bare names,
for when and until. wf.to_json() is the same document as the
YAML file. Client.from_config() reads ~/.config/astra/config.json
and the ASTRA_URL, ASTRA_TOKEN, ASTRA_ORG, ASTRA_WORKSPACE and
ASTRA_CLUSTER variables. The builder also has run, agent,
approval, wait_event, sleep, subflow, function, schedule
and limit (concurrency).
$ yq -o=json '{"metadata": .metadata, "spec": .spec}' message-triage.yaml \
| curl -sS -X POST "$ASTRALYX_API/anemoi/flows" -H "Authorization: Bearer $ASTRALYX_TOKEN" \
-H "Content-Type: application/json" -d @-
POST $ASTRALYX_API/anemoi/flow-checks with {"name", "spec"} checks
without writing: {"ok", "errors": [{"path", "message"}], "warnings":
[...]}. A new version is PUT $ASTRALYX_API/anemoi/flows/{name} with
{"spec", "current"}.
The checks refuse what cannot work when the workflow is written, not when it runs: a box that reads a box it does not need, an input that is not declared, a condition with two ways of the same name, a reference to another workspace. Warnings (an approval with no approvers, a wait with no time limit) do not stop it.
Run it#
Its trigger runs it when its address is called (see Start a workflow with an HTTP call). By hand:
On the workflow's page press Run now, fill in its inputs (and a
Version other than the current one, if you like) and press
Run. The execution's page opens. A workflow that reads event
also offers Event (JSON, optional): what a trigger would send, to
try it by hand.
$ astra iris workflows run message-triage --wait \
--event '{"id": "m1", "text": "The production database is down and customers cannot pay."}'
execution message-triage-5b2e90 of flow message-triage started
--event is JSON, or @file.json; --input key=value (repeatable)
gives inputs; --version N another version; --wait follows the
execution until it ends and prints its boxes and outputs (exit status
1 unless it Succeeded).
e = c.flows.run("message-triage", event={"id": "m1", "text": "The production database is down and customers cannot pay."})
done = c.executions.wait(e["metadata"]["name"], timeout=600)
print(done["status"]["state"], done.get("outputs"))
run(..., wait_seconds=60) answers once the execution has ended (at
most 90 s).
$ curl -sS -X POST "$ASTRALYX_API/anemoi/flows/message-triage/executions" \
-H "Authorization: Bearer $ASTRALYX_TOKEN" -H "Content-Type: application/json" \
-d '{"event": {"id": "m1", "text": "The production database is down and customers cannot pay."}, "wait_seconds": 60}' \
| jq '{state: .status.state, outputs}'
{inputs, version, event, wait_seconds}, each optional. Without
wait_seconds the answer is 201 at once; with it (1 to 90), 200
once the execution has ended, or 202 when it has not by then.
A run by hand runs as you, and applies none of a trigger's repeats or calls a minute.
Follow an execution#
The execution's page draws the graph live: each box's state (pending,
running, waiting, succeeded, failed, skipped), its attempts, their
reasons (Asking …, Waking deployment …, Took now, Notified),
links to their runs and approvals, the machine, the outputs; then the
execution's Trigger, Inputs (with its event), Outputs,
the Receipts of its agent runs and the Events it received.
Click a box to see only it.
$ astra iris workflows executions --flow message-triage
$ astra iris workflows get message-triage-5b2e90
$ astra iris workflows get message-triage-5b2e90 --json
get prints the state, then STEP STATE ATTEMPTS LAST MACHINE WHY
OUTPUTS, the receipts of its agent runs, and its outputs.
$ curl -sS "$ASTRALYX_API/anemoi/flow-executions?flow=message-triage&state=Running" -H "Authorization: Bearer $ASTRALYX_TOKEN"
$ curl -sS "$ASTRALYX_API/anemoi/flow-executions/message-triage-5b2e90?wait=30" -H "Authorization: Bearer $ASTRALYX_TOKEN" \
| jq '{state: .status.state, steps: (.steps | map_values({state, reason}))}'
?wait=<seconds> (at most 30) answers once the execution has ended,
or after that long: a long poll.
Send an event, retry a box, cancel#
An event goes to a wait_event box (the examples send deployed to one
so named; message-triage has none); a retry, to a box that failed.
On the execution's page: Send event on a waiting wait_event box
(its name and key=value data); Retry step on a failed box (a new
attempt); Cancel stops the boxes under way.
$ curl -sS -X POST "$ASTRALYX_API/anemoi/flow-executions/<execution>/events/deployed" -H "Authorization: Bearer $ASTRALYX_TOKEN" \
-H "Content-Type: application/json" -d '{"data": {"env": "prod"}}'
$ curl -sS -X POST "$ASTRALYX_API/anemoi/flow-executions/<execution>/steps/summary/retry" -H "Authorization: Bearer $ASTRALYX_TOKEN"
$ curl -sS -X POST "$ASTRALYX_API/anemoi/flow-executions/<execution>/cancel" -H "Authorization: Bearer $ASTRALYX_TOKEN"
An event's data is key=value text, at most 4 KiB. A retried approval or
event box asks again. DELETE $ASTRALYX_API/anemoi/flow-executions/{name}
removes an execution that ended, with its own drive.
Change a workflow#
Edit (new version) on the workflow's page opens the editor with the current version; tick Runs (and its triggers) use this version from now on or not, and press Write the version. Versions lists them, with Make current.
astra iris workflows apply -f again writes the next version;
astra iris workflows list shows FLOW CURRENT LATEST AGE.
Executions keep the version they started with. The current version's
triggers are the ones that fire. Delete on the workflow's page removes
it, its versions and its triggers' state; its executions stay, and its
addresses answer 404 HOOK_FLOW_GONE.