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 previous 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.<index> where <index> is the zero-based position of the step.
  • outputs.<id> where <id> is the optional id you assigned to the step.

Example 1: Processing Files with for_each

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

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

- 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"

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:

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

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

Here:

  1. The first run step (index 0) captures the current date.
  2. The second run step uses {{ outputs.0 }} twice: once to include the date in the file content and again to dynamically name the report file (e.g., 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

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:

  - 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."