Templates
Prompts, commands, approval messages, runIf conditions, branch names and subagent prompts are templates. Jimothy’s template engine is small and dependency-free: {{ }} placeholders with dot paths, filters, and {{#if}} blocks.
Issue {{issue.key}}: {{issue.title}}{{#if issue.url}}Link: {{issue.url}}{{/if}}
{{issue.description}}
Plan from the planning agent:{{steps.plan.output | truncate:6000}}{{#if steps.review.output}}A reviewer requested changes. Address every point:{{steps.review.output}}{{else}}This is the first pass.{{/if}}Where templates are used
Section titled “Where templates are used”| Field | Rendered | Shell-quoted |
|---|---|---|
| Agent Prompt | when the step starts (each attempt/iteration) | no |
| Agent System prompt | when the step starts | no |
| Subagent System prompt | when an agent step that uses it starts, with that step’s context | no |
| Shell Command | when the step starts | yes |
| Message shown to the approver | when the approval starts; displayed as Markdown | no |
| Run only if | when the step becomes ready | no |
| Branch name template | once, when the workspace is prepared | no (sanitized into a valid branch name instead) |
Harness argument templates ({{prompt}}, {{model}}, {{systemPrompt}}, {{promptFile}}, {{subagents}}, {{cwd}}, {{options}}) use the same syntax but a different, harness-specific context. See Custom harnesses.
Values
Section titled “Values”{{path}} looks up a dot path in the template context. Whitespace inside the braces is ignored: {{ issue.title }} works.
{{issue.key}} → ENG-42{{run.number}} → 17{{steps.plan.output}} → the plan step's output{{vars.testCommand}} → npm test{{issue.raw.teamName}} → a provider-specific field, if presentHow values become text:
| Value | Rendered as |
|---|---|
missing path, null, undefined |
empty string (no error) |
| string | as-is |
| number, boolean | String(value), e.g. 17, true |
| array of strings/numbers | joined with , , e.g. bug, auth |
| object, or array of objects | pretty-printed JSON (2-space indent) |
A quoted string is a literal, handy with filters: {{"Hello World" | slug}} → hello-world.
Values are inserted once and never re-rendered: if an issue description contains {{issue.key}} or {{#if}}, it’s inserted literally. Issue text can’t inject template syntax.
Step ids may contain - and _; paths split only on ., so {{steps.run-tests.output}} works.
Template context
Section titled “Template context”| Path | Value |
|---|---|
issue.key |
Issue key: ENG-123, PROJ-45, #12, or TASK-7 for manual runs without a key |
issue.title |
Title |
issue.description |
Body as plain text |
issue.url |
Link to the issue |
issue.labels |
Labels (renders comma-separated) |
issue.priority, issue.status, issue.assignee, issue.reporter |
When the source provides them |
issue.source |
jira, linear, github, manual, webhook or schedule |
issue.id, issue.updatedAt |
Provider id and last update time |
issue.raw.* |
Extra provider-specific fields |
run.id, run.number |
Run id and number |
run.branch |
The run’s git branch (empty until the workspace is prepared, so empty in the branch template itself) |
run.baseBranch |
Base branch, default main |
run.workspace |
Absolute workspace path |
pipeline.id, pipeline.name |
The pipeline |
project.id, project.name |
The project; empty if the pipeline has none |
vars.NAME |
Variables: project commands, project variables, pipeline variables, run overrides. See Variables & precedence |
steps.<id>.output |
The step’s output (last 20,000 chars); empty if it hasn’t run |
steps.<id>.status |
pending, running, awaiting_approval, succeeded, failed, skipped, cancelled |
steps.<id>.error |
The step’s error message; empty if none |
steps.<id>.comment |
Approval steps: the approver’s comment (cleared when a loop or retry resets the step; output also holds the comment and survives the reset) |
steps.<id>.approvedBy |
Approval steps: who approved or rejected (cleared on reset, like comment) |
steps.<id>.selected |
Approval steps with choices: the full Markdown of each picked option, separated by a blank line (cleared on reset, like comment). See Picking from options |
steps.<id>.selectedIds |
The picked option numbers, e.g. 1, 3 |
steps.<id>.selectedTitles |
The picked option titles, separated by ; |
loop.iteration |
How many times the current step has been re-entered by a feedback loop (0 on the first pass) |
The complete, per-source list of issue fields is in the template variables reference. For runs without an issue at all, issue defaults to key RUN-<number>, the pipeline name as title, and an empty description.
The context is built fresh for each attempt, so steps.* reflects the latest results of every step at that moment.
Filters
Section titled “Filters”Apply filters with |. They run left to right, and take an optional argument after :, quoted or not.
{{issue.key | slug}}{{steps.test.output | truncate:4000}}{{issue.priority | default:"none"}}{{issue.title | trim | lower | slug:30}}| Filter | Argument | Result | Example |
|---|---|---|---|
default |
text | The argument if the value is missing, null or an empty string; otherwise the value |
{{issue.priority | default:"none"}} → none |
slug |
max length, default 48 |
Lowercase ASCII slug: accents stripped, runs of non-alphanumerics become -, leading/trailing - removed, cut to length |
{{" Héllo, World!! " | slug}} → hello-world |
lower |
— | Lowercase | {{issue.key | lower}} → eng-42 |
upper |
— | Uppercase | {{issue.title | upper}} → FIX USER'S LOGIN |
trim |
— | Leading/trailing whitespace removed | |
first_line |
— | Only the first line | {{steps.plan.output | first_line}} |
truncate |
max characters, default 2000 |
Cut to N characters with … appended if longer |
{{issue.title | truncate:3}} → Fix… |
json |
— | JSON.stringify of the value (null if missing) |
{{issue.labels | json}} → ["bug","auth"] |
shell |
— | POSIX single-quoted string | {{issue.title | shell}} → 'Fix user'\''s login' |
raw |
— | The value unchanged; in shell commands, disables automatic quoting when it’s the last filter | {{vars.testCommand | raw}} |
Unknown filter names are ignored (the value passes through unchanged), so a typo doesn’t fail the run but also doesn’t do what you meant.
truncate keeps the start of the text. Step outputs are already cut to their last 20,000 characters when recorded, so for long logs you may want a small truncate limit to keep prompts focused.
Conditionals
Section titled “Conditionals”{{#if path}}shown when truthy{{/if}}{{#if path}}truthy branch{{else}}falsy branch{{/if}}- The condition is any expression, including filters:
{{#if steps.lint.output | trim}}. - Blocks nest:
{{#if a}}A{{#if b}}B{{else}}not B{{/if}}{{else}}not A{{/if}}. - Truthiness:
null/missing → false- arrays → true if non-empty (so
{{#if issue.labels}}means “has labels”) - strings → false if, trimmed and lowercased, they are
'',false,0orno; true otherwise - numbers and booleans → as in JavaScript (
0is false) - objects → true
- There’s no
{{#unless}},{{#each}}, or comparison (==,contains). Use{{#if x}}{{else}}…{{/if}}for “unless”, and put decisions that need comparison in an earlier step whose output you test. - Conditionals are resolved before values are inserted, and the text inside a block is template text like any other.
Shell quoting
Section titled “Shell quoting”In shell step commands (and only there), every interpolated value is quoted for the shell automatically, so issue titles, descriptions and step outputs can’t inject commands:
echo {{issue.title}}With the title Fix user's login; rm -rf ~, this renders on macOS/Linux as:
echo 'Fix user'\''s login; rm -rf ~'The rules:
- Every
{{…}}interpolation is quoted after all its filters run. Values are wrapped in single quotes ('becomes'\''). | rawas the last filter turns quoting off for that interpolation. Use it for values that are themselves commands or shell syntax, such as{{vars.testCommand | raw}}or{{vars.setupCommand | raw}}.rawanywhere but last doesn’t disable quoting.- Don’t add your own quotes around placeholders.
"{{issue.title}}"renders as"'Fix user'\''s login'", and the shell prints the single quotes literally. Write{{issue.title}}bare. - Don’t use
| shellin shell steps: the result would be quoted twice. It’s for other contexts that need a quoted string. - Empty values render as
'': an empty argument, not nothing.git commit -m {{steps.summary.output}}with no output passes-m ''. - Literal text in the command, including text inside
{{#if}}blocks, is not quoted. Only interpolated values are. - Other fields (prompts, system prompts, approval messages,
runIf, branch templates) are never shell-quoted.
Environment variables are often the cleanest alternative. Shell steps get FACTORY_ISSUE_KEY, FACTORY_ISSUE_TITLE, FACTORY_ISSUE_URL, FACTORY_ISSUE_DESCRIPTION, FACTORY_BRANCH, FACTORY_BASE_BRANCH, FACTORY_RUN_NUMBER, FACTORY_RUN_ID, FACTORY_WORKSPACE, FACTORY_PIPELINE, FACTORY_PROJECT and FACTORY_STEP_ID, and you control the quoting:
gh pr create --base {{run.baseBranch}} --head {{run.branch}} \ --title "$FACTORY_ISSUE_KEY: $FACTORY_ISSUE_TITLE" \ --body "Automated by Jimothy run #$FACTORY_RUN_NUMBER. $FACTORY_ISSUE_URL"Here {{run.baseBranch}} and {{run.branch}} are quoted by Jimothy, and the title and body use double-quoted environment variables. See Environment variables.
Quick reference
Section titled “Quick reference”| You write (shell step) | Renders as (POSIX) | Notes |
|---|---|---|
echo {{issue.key}} |
echo 'ENG-42' |
Safe |
{{vars.testCommand | raw}} |
npm test -- --run |
Runs as a command |
{{vars.testCommand}} |
'npm test -- --run' |
Fails: the shell looks for a program named npm test -- --run |
echo "{{issue.title}}" |
echo "'Fix it'" |
Prints the quotes; drop the double quotes |
echo "$FACTORY_ISSUE_TITLE" |
echo "$FACTORY_ISSUE_TITLE" |
Env var, your quoting |
curl -d {{issue | json}} … |
curl -d '{"source":…}' … |
JSON as one argument |
Gotchas
Section titled “Gotchas”- A missing path renders as empty without an error. Double-check step ids:
{{steps.reveiw.output}}is silently empty. The editor’s variable chips insert valid paths. {{run.branch}}is empty inside the branch name template (the branch doesn’t exist yet). Use{{run.number}}and{{issue.*}}there.issue.labelsrenders asbug, auth, not JSON. Use| jsonif a tool needs an array.- Unclosed
{{#if}}blocks aren’t reported as errors; the tags are left in the text. {{else}}outside an{{#if}}renders as empty.