Skip to main content

Templating

Bosun workflows use a templating system to create dynamic, repeatable, and flexible tasks. Templating allows you to inject values, manipulate data, and generate instructions or configurations on the fly, making your workflows adaptable to various scenarios without hardcoding every detail.

Templates are typically enclosed in double curly braces {{ ... }} and can be used in any field that accepts a string value. Under the hood, Bosun leverages the Tera templating engine (inspired by Jinja2 and Django templates).

Templates can include:

  • References to outputs of earlier steps: Accessing data generated by earlier parts of the workflow.
  • Loop variables: Using the current item when iterating over a list.
  • Built-in functions and filters: Performing operations like splitting strings, slicing lists, or encoding data.

Tera helpers such as split, slice, filter, json_encode, format, and upper are all available—you can mix them the same way the upstream Tera examples do. Bosun resolves the expressions before executing a step so every agent, prompt, or shell command receives a fully rendered string.

Template guardrails
  • Use json_encode() before handing values to for_each or structured prompts so Bosun receives valid JSON.
  • Prefer quote or %{} shell expansions instead of direct interpolation when using templates inside run commands to avoid globbing surprises.

Accessing Step Outputs (outputs)

Each step in a Bosun workflow can produce an output. You can reference these outputs in subsequent steps using either:

  • outputs.<id> where <id> is the step ID you assigned in the manifest.
  • outputs.<index> where <index> is the zero-based position of the step. This still works, but IDs are easier to maintain in graph manifests.

Example 1: Processing Files with for_each

Consider this workflow snippet that finds configuration files and processes each one:

steps:
- id: scan_configs
name: Get configuration files
run: find . -name "*.config.json"

- id: process_configs
name: Process each configuration
for_each:
from: '{{ outputs.scan_configs | split(pat="\n") | slice(end=inputs.maxFiles) | json_encode() }}'
agent:
extends: Coding
instructions: "Validate and standardize the configuration in file {{ for_each.value }}"
role: "You are an expert in configuration management."
constraints:
- "Ensure all configuration values are valid JSON"
- "Apply standard formatting to the file"
- "Report any invalid configurations without modifying them"
- "Focus solely on the specified file"

starts_with: scan_configs
ends_with: process_configs

edges:
- from: scan_configs
to: process_configs

In this example:

  1. scan_configs step: Runs find to list .config.json files and stores the results under the ID scan_configs.
  2. for_each step: Reads the output via outputs.scan_configs, splits it into a list, slices it using an input value (see below), and iterates over each path.
  3. Agent instructions: {{ for_each.value }} inserts the current file path so the agent edits the correct file.

Example 2: Accepting Numeric Inputs

Task inputs can be typed as numbers. Use the Number type in your manifest and reference the value directly inside templates:

inputs:
maxFiles:
type: Number
required: false
description: Limit how many files to process (defaults to 3).
default: 3

Bosun fills the default value whenever a run or UI trigger omits the input. Defaults are resolved before any step executes, so the template context always exposes a concrete value. You can still override maxFiles by providing values via the CLI values block or the UI input form.

Inside templates the numeric input behaves like any other value. In the snippet above it controls how many files are processed: slice(end=inputs.maxFiles).

Where default expressions can point

Default expressions are rendered with the same Tera engine, but they are evaluated eagerly inside the manifest. They can reference other declared inputs (e.g. {{ inputs.baseDir }}/src) or session_id. Runtime-only context such as outputs, errors, or for_each is unavailable at this stage and will cause validation errors in the manifest.

Example 3: Dynamic run Commands

Templating can also be used directly within run commands to create dynamic shell scripts or file operations:

steps:
- id: get_current_date
name: Get current date
run: date +%Y-%m-%d

- id: create_dated_report
name: Create a dated report file
run: echo "Report generated on {{ outputs.get_current_date }}" > report-{{ outputs.get_current_date }}.txt

starts_with: get_current_date
ends_with: create_dated_report

edges:
- from: get_current_date
to: create_dated_report

Here:

  1. The get_current_date step captures the current date.
  2. The create_dated_report step uses {{ outputs.get_current_date }} twice: once to include the date in the file content and again to dynamically name the report file (for example report-2023-10-27.txt).

Template validation before a run

Bosun validates every template when you save a manifest or trigger a run. The validator walks each string field (including nested for_each steps) and guarantees that every expression only references data that exists inside the manifest. Unknown references block the run before any step code executes, so you can fix template issues without burning credits.

Allowed context globals:

  • session_id
  • inputs
  • outputs
  • errors
  • for_each
  • step_statuses

Pre-runtime fields: repositories and working directories

Repository definitions and every step-level working_directory render before any code runs. Bosun performs two passes:

  1. Repository definitions render first and can only reference inputs and session_id.
  2. After the repositories are cloned, Bosun renders each working_directory with access to inputs, session_id, and repositories.

The repositories object exposes one entry per manifest repository (plus a primary alias). Each entry contains name and path, where path is the absolute executor path to that checkout.

Runtime-only globals such as outputs, errors, step_statuses, or nested for_each data remain unavailable to pre-runtime fields and cause validation errors when referenced from repositories or working_directory values.

inputs:
service_branch:
type: String
required: true
repositories:
- name: primary
primary: true
base_branch: "{{ inputs.service_branch }}"
- name: docs
provider:
type: github
url: https://github.com/acme/docs

steps:
- id: run-docs-tests
name: Run docs tests from repo clone
working_directory: "{{ repositories.docs.path }}/website"
run: bun test

starts_with: run-docs-tests
ends_with: run-docs-tests
edges: []

In this manifest the primary repository tracks a user-provided branch, and the run step scopes itself to the docs repository checkout using the resolved path.

Within each global the validator enforces more granular rules:

  • inputs.<name> must point at a declared input or values entry.
  • outputs.<id> and errors.<id> must reference a step that already has that id (numeric indexes continue to work for both collections).
  • step_statuses.<id> exposes the success state for any step with an id.
  • for_each.value and for_each.idx are only valid inside the body of a for_each step, and for_each.outputs.<nested_id> only works when the nested step includes that id.

Example: instruction interpolation in a for_each

This snippet mirrors the linter example and shows how templating behaves inside iterators:

steps:
- id: lint_errors
run: ./scripts/list-lint-errors.sh

- id: fix_lint_errors
name: Fix each linter error with an agent
for_each:
from: '{{ outputs.lint_errors | split(pat="\n") | filter(value) | slice(end=inputs.maxErrors) | json_encode() }}'
agent:
extends: Coding
instructions: |
Apply automatic lint fixes to {{ for_each.value }}.
Focus solely on the specified file: {{ for_each.value }}
role: "You are a meticulous code formatter and linter expert."
constraints:
- "Only apply fixes that do not change code logic."

starts_with: lint_errors
ends_with: fix_lint_errors

edges:
- from: lint_errors
to: fix_lint_errors