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.
- Use
json_encode()before handing values tofor_eachor structured prompts so Bosun receives valid JSON. - Prefer
quoteor%{}shell expansions instead of direct interpolation when using templates insideruncommands 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 optionalidyou 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:
scan_configsstep: Runsfindto list.config.jsonfiles and stores the results under the IDscan_configs.for_eachstep: Reads the output viaoutputs.scan_configs, splits it into a list, slices it using an input value (see below), and iterates over each path.- 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).
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:
- The first
runstep (index 0) captures the current date. - The second
runstep 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_idinputsoutputserrorsfor_eachstep_statuses
Within each global the validator enforces more granular rules:
inputs.<name>must point at a declared input orvaluesentry.outputs.<id>anderrors.<id>must reference a step that already has thatid(numeric indexes continue to work for both collections).step_statuses.<id>exposes the success state for any step with anid.for_each.valueandfor_each.idxare only valid inside the body of afor_eachstep, andfor_each.outputs.<nested_id>only works when the nested step includes thatid.
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."