Skip to content

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

{{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 present

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

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.

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.

{{#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, 0 or no; true otherwise
    • numbers and booleans → as in JavaScript (0 is 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.

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:

Terminal window
echo 'Fix user'\''s login; rm -rf ~'

The rules:

  1. Every {{…}} interpolation is quoted after all its filters run. Values are wrapped in single quotes (' becomes '\'').
  2. | raw as 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}}. raw anywhere but last doesn’t disable quoting.
  3. 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.
  4. Don’t use | shell in shell steps: the result would be quoted twice. It’s for other contexts that need a quoted string.
  5. Empty values render as '': an empty argument, not nothing. git commit -m {{steps.summary.output}} with no output passes -m ''.
  6. Literal text in the command, including text inside {{#if}} blocks, is not quoted. Only interpolated values are.
  7. 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:

Terminal window
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.

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
  • 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.labels renders as bug, auth, not JSON. Use | json if 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.