Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2f36b5f
refactor(workflow): move read checks out of parse.ts into reads.ts
SaulMoro Oct 15, 2026
ea2c9de
refactor(workflow): parse step paths in one place
SaulMoro Oct 15, 2026
f22b8db
refactor(workflow): model step state as a zod union with transitions
SaulMoro Oct 15, 2026
1fe635b
test(workflow): pin the stored shape of an inline workflow step
SaulMoro Oct 15, 2026
5533332
feat(workflow): settle in two phases and lease steps in parallel
SaulMoro Oct 15, 2026
6663c26
feat(workflow): check reports against their own lease and order ends …
SaulMoro Oct 15, 2026
4bad0db
feat(workflow): cancel what still runs when a workflow reaches an end
SaulMoro Oct 15, 2026
30684a0
feat(workflow): read array elements by index
SaulMoro Oct 15, 2026
d8ad516
feat(workflow): add comparisons and list functions to expressions
SaulMoro Oct 15, 2026
86cd5e7
feat(workflow): run a step's loop again and keep every pass
SaulMoro Oct 15, 2026
7f5c55f
feat(workflow): read a later step of the same loop in its previous pass
SaulMoro Oct 15, 2026
1d06332
feat(workflow): loop a workflow round after round, and end it with break
SaulMoro Oct 15, 2026
5ef17f8
docs(workflow): split the guide into base and advanced pages
SaulMoro Oct 15, 2026
e0739c6
feat(workflow): run local commands in parallel up to workflows.max_pa…
SaulMoro Oct 15, 2026
ea4dfa3
docs(workflow): document step loops, workflow loops, and local parall…
SaulMoro Oct 15, 2026
1ebc2f2
refactor(workflow): list a step's expressions in one place
SaulMoro Oct 15, 2026
dadcfeb
feat(workflow): run a step once per item with for_each
SaulMoro Oct 15, 2026
49a0e4d
test(workflow): cover a step loop and a parallel for_each with the re…
SaulMoro Oct 15, 2026
25ade5a
fix(workflow): record loop passes, cancelled steps and the failure code
SaulMoro Oct 15, 2026
e6721e1
fix(workflow): report a run record only when one was queued
SaulMoro Oct 15, 2026
b1c637c
perf(workflow): settle a for_each without rescanning every step
SaulMoro Oct 15, 2026
a6c1a91
feat(workflow): evaluate an expression inside a template
SaulMoro Oct 15, 2026
b394837
test(workflow): check the guide's --inputs examples against their wor…
SaulMoro Oct 15, 2026
b88b294
fix(workflow): read a record entry's agent from the definitions
SaulMoro Oct 15, 2026
6ed070e
refactor(workflow): make stopUnder exhaustive over step statuses
SaulMoro Oct 16, 2026
e2580aa
test(workflow): check the guide's lease and results JSON against the …
SaulMoro Oct 16, 2026
58ee45f
fix(workflow): compare deeply nested values without overflowing the s…
SaulMoro Oct 16, 2026
00b4581
fix(workflow): keep a report's free text out of the run record's tool…
SaulMoro Oct 16, 2026
c626642
fix(workflow): keep stopping a cancelled command until its process gr…
SaulMoro Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions docs/dev/workflow-engine.md

Large diffs are not rendered by default.

575 changes: 575 additions & 0 deletions docs/guide/workflow-advanced.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions docs/guide/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Part of the [TeamAI CLI Usage Guide](../usage-guide.md).

A workflow is a YAML file at `workflows/<name>.yaml` in the team repo. It runs command and read-only prompt steps, each after the steps it needs.
A workflow is a YAML file at `workflows/<name>.yaml` in the team repo. It runs command and read-only prompt steps, each after the steps it needs. [Advanced workflows](workflow-advanced.md) covers parallel steps, loops, `for_each`, what an end does to steps that still run, and the full expression language.

```yaml
schemaVersion: v1alpha1
Expand All @@ -24,9 +24,9 @@ steps:

`schemaVersion` must be `v1alpha1`. `name` must match the file name. `description` is required. Each step has a unique `id` and exactly one of `command`, `prompt`, `prompt_file`, or `workflow`. `agent` is optional on prompt steps. `output` uses the existing shorthand: `string`, `number`, `boolean`, a literal union, an object, or an array with one item type. TeamAI validates successful command stdout or a prompt tool's `output` against it.

Every step after the first lists the steps it waits for in `needs`. `needs: []` starts another branch with no dependency. One step runs at a time: the first step in the file whose needs are done. A step is skipped when a step it needs failed, or when every step it needs was skipped. A skipped step never runs. A failure does not stop other branches. When nothing is left to run, the run ends `failed` if any step failed, otherwise `succeeded`. A missing `needs`, a `needs` id that is not a step, or a cycle makes the file invalid. A missing `needs` also leaves the workflow with nothing to run, even when an older version was valid.
Every step after the first lists the steps it waits for in `needs`. `needs: []` starts another branch with no dependency. Each `next` leases the first step in the file whose needs are done. `--driver local` runs one step at a time unless `workflows.max_parallel` is above 1. A step is skipped when a step it needs failed, or when every step it needs was skipped. A skipped step never runs. A failure does not stop other branches. When nothing is left to run, the run ends `failed` if any step failed, otherwise `succeeded`. A missing `needs`, a `needs` id that is not a step, or a cycle makes the file invalid. A missing `needs` also leaves the workflow with nothing to run, even when an older version was valid.

A step can carry an `if` expression. When it is false, the step is skipped, and that does not fail the run. An expression reads `steps.<id>.outcome` (`success`, `failed`, or `skipped`) and `steps.<id>.outputs.<field>`, only for steps it needs, directly or through their needs. It has `==`, `!=`, `&&`, `||`, `!`, parentheses, and the literals `'text'`, numbers, `true`, `false`, `null`, and `[]`. A missing value reads as `null`. `false`, `null`, `0`, and `''` are false; everything else is true. `==` is strict and compares whole values: `'1' == 1` and `'Build' == 'build'` are false.
A step can carry an `if` expression. When it is false, the step is skipped, and that does not fail the run. An expression reads `steps.<id>.outcome` (`success`, `failed`, or `skipped`) and `steps.<id>.outputs.<field>`, only for steps it needs, directly or through their needs. It has `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `!`, parentheses, and the literals `'text'`, numbers, `true`, `false`, `null`, and `[]`. A path can index an array, as in `steps.review.outputs.files[0]`. The functions `length`, `any`, `all`, and `filter` read arrays. A missing value reads as `null`. `false`, `null`, `0`, and `''` are false; everything else is true. `==` is strict and compares whole values: `'1' == 1` and `'Build' == 'build'` are false.

A step whose dependency failed still runs when its `if` reads every failed dependency. Use this to recover:

Expand All @@ -45,7 +45,7 @@ A workflow can declare named `ends`, each `succeeded` or `failed`, such as `ends

A workflow can declare `inputs` with the same shorthand as `output`, such as `inputs: { issue: number, title: string }`. Every input is required. Pass them as a JSON object: `teamai workflow start fix --driver local --inputs '{"issue":42,"title":"Crash"}'`. A missing, extra, or mistyped input returns `invalid_inputs` naming the field, and no run is created. An `if` can read `inputs.<name>`.

`{{ inputs.<name> }}` and `{{ steps.<id>.outputs.<field> }}` (or `.outcome`) fill in a `prompt`, the text of a `prompt_file`, and `env` values when the step starts. Other `{{ ... }}` text, such as `${{ secrets.TOKEN }}`, stays as written. So does any `{{` right after a `$`, such as `${{ inputs.version }}` in a prompt about GitHub Actions. To put a value right after a `$`, add a space: `$ {{ inputs.price }}`. Strings appear as they are, numbers and booleans as text, objects and arrays as JSON, and `null` or a missing value as empty text. A template reads only what an `if` on that step could read.
`{{ inputs.<name> }}` and `{{ steps.<id>.outputs.<field> }}` (or `.outcome`) fill in a `prompt`, the text of a `prompt_file`, and `env` values when the step starts. A template can also evaluate an expression as an `if` does, such as `{{ length(steps.review.outputs.files) }}`. Other `{{ ... }}` text, such as `${{ secrets.TOKEN }}`, stays as written. So does any `{{` right after a `$`, such as `${{ inputs.version }}` in a prompt about GitHub Actions. To put a value right after a `$`, add a space: `$ {{ inputs.price }}`. Strings appear as they are, numbers and booleans as text, objects and arrays as JSON, and `null` or a missing value as empty text. A template reads only what an `if` on that step could read.

A `command` is never filled in. Pass values to it through `env`, which only a command step can have:

Expand Down Expand Up @@ -112,7 +112,7 @@ An agent-specific entry wins over `default`. `claude`, `codex`, and `opencode` a
teamai workflow start build --driver local
```

`--driver local` runs one step at a time in the current directory. Each attempt uses `result.json` under the scope's TeamAI data directory, outside the checkout. Prompt steps are read-only: if a tool changes the repository, the step fails and leaves those files in place for inspection. This includes changes to files inside an existing untracked directory; leaving that directory unchanged does not fail the step. The driver calls the same `next` and `report` operations shown below:
`--driver local` runs steps in the current directory, one at a time unless [`workflows.max_parallel`](workflow-advanced.md#run-local-commands-in-parallel) is above 1. Each attempt uses `result.json` under the scope's TeamAI data directory, outside the checkout. Prompt steps are read-only: if a tool changes the repository, the step fails and leaves those files in place for inspection. This includes changes to files inside an existing untracked directory; leaving that directory unchanged does not fail the step. The driver calls the same `next` and `report` operations shown below:

```text
teamai workflow start build
Expand All @@ -128,7 +128,7 @@ The run is stored in this scope's TeamAI data directory. The command exits when

With [usage reporting](reporting.md) on, every run publishes a privacy-preserving record to the team's `teamai-reports` branch: once when it starts and once when it ends. With it off, nothing is published and the envelopes carry no `record`. Records live in `workflows/runs/<run id>.json`, keyed by run id, so a retried publication updates the same record instead of adding a copy.

A record carries only: the run id, workflow name, phase (`start` or `end`), driver kind, status, the end the run reached (when it reached one), start and end times, duration, per-step entries (step path such as `check.classify`, agent, tool, model, outcome, tokens — a mixed-agent run records each step's own fields), summary step counts (succeeded, failed, skipped), the cumulative settled-attempt count, and the publishing username. Steps of a sub-workflow that never started are not counted. Prompts, command text, input values, `env` values, outputs, diffs, secret values, local workspace paths, session ids, and hostnames have no field to land in — the record is built from an allowlist, not filtered from the run state.
A record carries only: the run id, workflow name, phase (`start` or `end`), driver kind, status, the end the run reached (when it reached one), the failure code of a run that failed without reaching an end, when it is a code teamai sets, such as `loop_exhausted` for a loop that ran out or `tool_unavailable` when the local driver cannot find the tool (never its message), start and end times, duration, per-step entries (step path such as `check.classify`, agent, tool, model, outcome, tokens — a mixed-agent run records each step's own fields; `tool` is kept only when it is one of the local driver's tools, and `model` is the name the driver reported, kept only when it is shaped like a model name), summary step counts (succeeded, failed, skipped, cancelled), the cumulative settled-attempt count, and the publishing username. Every entry has its `pass` number, and a step that a loop ran more than once has one entry per pass. An item of a `for_each` counts the times that item ran, so its `pass` can be lower than its step's when an earlier pass had fewer items. A cancelled step's entry has the outcome `cancelled`, and a skipped step's has none. The counts cover each step once, as the run ended. Steps of a sub-workflow that never started are not counted. Prompts, command text, input values, `env` values, outputs, diffs, secret values, local workspace paths, session ids, and hostnames have no field to land in — the record is built from an allowlist, not filtered from the run state.

The end record is published by the `next` that settles the run. A publication that fails — no remote, a busy reports lock, a rejected push — is queued in this scope's data directory and retried on the next `teamai pull`. The `start` and `next` envelopes then carry `record: pending` instead of `record: published`. The run itself is unaffected: a machine that dies after starting a run leaves it visible as started but not ended.

Expand Down
Loading
Loading