Skip to main content

Task Graphs and Branching

Bosun tasks can be expressed as graphs so you can branch, merge, and conditionally skip steps without writing imperative control flow. The manifest still lists the individual steps, but you describe how they connect by declaring start and end nodes plus the transitions between them. Input definitions and their default values continue to live under the top-level inputs block, so the same manifest fields work for sequential and graph workflows alike.

Graph Manifest Structure

A graph manifest extends the familiar task format with three top-level fields:

  • starts_with: the id of the first step Bosun should execute.
  • ends_with: the id of the terminal step. When Bosun reaches this node, the workflow exits.
  • edges: an array of directed connections between step IDs. Each edge optionally declares an on condition.

Every step in a graph needs a stable id. If you omit them, Bosun generates defaults such as step-1, but naming them yourself makes edges easier to read.

Name steps before wiring edges

Assign the id field as soon as you draft the step so you never have to hunt down autogenerated names like step-3 when connecting edges later.

name: update-dependencies
description: Run tests, try an autofix, and only open a PR if everything passes.

steps:
- id: run-tests
name: Run test suite
agent:
extends: Coding
instructions: "Execute npm test and summarise the result."

- id: try-fix
name: Attempt automatic fix
agent:
extends: Coding
instructions: "Apply straightforward fixes for the failing tests."

- id: open-pr
name: Prepare pull request
agent:
extends: pull_request

starts_with: run-tests
ends_with: open-pr

edges:
- from: run-tests
to: try-fix
on: failure
- from: run-tests
to: open-pr
on: success
- from: try-fix
to: run-tests
on: success

Edge Conditions

Transitions default to success when no on value is set. You can also choose:

  • failure: trigger when the source step reports a failure.
  • A custom expression: Bosun renders the string as a template in the current task context and treats any non-empty, non-false/0 value as truthy.

For example, you can branch on the contents of a previous step output:

  - from: validate-report
to: notify-team
on: '{{ outputs.validate-report.result == "changes_required" }}'

Keep expressions short and render them to clear true/false strings. If an expression fails to render or resolves to an empty value, Bosun falls back to evaluating the next available edge.

Each step can declare at most one success edge and one failure edge. Custom conditions are evaluated before the success fallback in the order they appear in the manifest.

Branching on structured failures

Failure edges become much more powerful when you pair them with Custom Schemas. Agents can return rich JSON through stop_schema/fail_schema, Bosun stores that data under errors.<step>[i].reason, and your graph edges decide where to go next based on those fields.

A Bosun regression-manifest fixture shows a simple version of this pattern: an agent required to call task_failed with { summary, blockers, blocking } fans out to a recovery step via an on: failure edge. You can take the same idea further by looking inside the payload.

Example: fan out work when an agent fails

steps:
- id: regression-auditor
name: Audit upgraded services
agent:
extends: Coding
instructions: |
Run the regression suite. If anything fails, stop immediately and call `task_failed` with the services that need manual intervention.
fail_schema:
type: object
required: [summary, retryable, owners]
properties:
summary:
type: string
retryable:
type: boolean
owners:
type: array
items:
type: object
required: [service, assignee]
properties:
service:
type: string
assignee:
type: string

- id: notify-success
run: echo "All services passed" > status.txt

- id: dispatch-fixers
for_each:
from: '{{ errors.regression-auditor[0].reason.owners | json_encode() }}'
parallel: true
agent:
extends: Coding
instructions: |
Address the regression in {{ for_each.value.service }} and hand it back to {{ for_each.value.assignee }}.

- id: summarize
agent:
extends: pull_request
instructions: "Open or update the PR once all owners report back."

starts_with: regression-auditor
ends_with: summarize

edges:
- from: regression-auditor
to: notify-success
on: success
- from: regression-auditor
to: dispatch-fixers
on: failure
- from: dispatch-fixers
to: summarize

How it works:

  1. regression-auditor either succeeds and flows to notify-success, or fails with a structured payload.
  2. The failure edge routes control to dispatch-fixers, which turns the list of owners from errors.regression-auditor[0].reason into a parallel for_each run. Each iteration inherits the service/assignee details from the failure payload.
  3. When the fan-out loop finishes, the graph continues to summarize, where you can aggregate the fixes or open a PR.

Because the schema is enforced at runtime, you can rely on fields such as retryable or owners existing before you branch. If you only need to check a flag, add a conditional expression to the edge instead:

edges:
- from: regression-auditor
to: dispatch-fixers
on: '{{ errors.regression-auditor[0].reason.retryable == true }}'
- from: regression-auditor
to: escalate
on: failure