Skip to main content

Agents & Actions

Action Template Variables

When an action runs, Spruce substitutes a small set of template variables into a mode's args / env (or the legacy top-level command: string) and into the markdown body before launch. The same {{name}} placeholders work in either place.

Variables

VariableResolves to
{{artifact_id}}The artifact's ID (e.g. SPR-abc123)
{{session_id}}The unique ID of the terminal session running this action
{{action_content}}The action's markdown body, used to inject the prompt into a flag like --system-prompt
{{current_time}}Current date and time, ISO 8601
{{artifact_path}}Absolute path to the current artifact's .md file
{{code_path}}Path to the code root: the worktree path when working in a worktree, otherwise the project root
{{project_path}}Path to the project root (the folder that contains .spruce/)
{{template_path}}Path to the templates directory (.spruce/templates/)
{{sidecar_path}}Absolute path to the bundled spruce-cli binary (falls back to "spruce" when the bundled sidecar can't be resolved). The raw path — fine for bare argv slots, but for inline JSON / TOML config use {{sidecar_path_jsonsafe}} instead (see below).
{{sidecar_path_jsonsafe}}Same as {{sidecar_path}} but with backslash and double-quote characters escaped so the value can be dropped inside a JSON or TOML basic-string "...". Use this in per-mode MCP config snippets; the raw form would emit invalid JSON on Windows because the path contains \.
{{session_file:<name>}}Absolute path to a per-session file declared in the mode's session_files: map. The file is written under <tmp>/spruce-sessions/<session_id>/<name> after its own {{placeholder}}s are resolved. Used by agents that take MCP config via a settings file rather than a flag (e.g. Gemini's GEMINI_CLI_SYSTEM_SETTINGS_PATH).
{{template:<type>}}Inline content of a template definition (e.g. {{template:task}})
{{context:<name>}}Inline content from a context file (e.g. {{context:artifact-system}}, {{context:comment-system}})

How {{action_content}} works

{{action_content}} is the canonical way to pass the action's prompt to an agent. Inside a mode's args:, the body lands in whatever flag the agent uses for its system prompt:

yaml

modes:
  claude:
    binary: claude
    args:
      - --system-prompt
      - "{{action_content}}"

After substitution, the spawned argv is ["claude", "--system-prompt", "<the markdown body, with all other variables already substituted>"]. Inside the body, you can still use other variables; they're resolved before the substitution happens, so {{artifact_id}} inside the prompt becomes the actual ID by the time the agent sees it.

How {{sidecar_path}} and {{sidecar_path_jsonsafe}} work

Both resolve to the absolute path of the bundled spruce-cli binary (e.g. /Applications/Spruce.app/Contents/MacOS/spruce-cli on macOS, C:\Users\me\AppData\Local\Programs\Spruce\spruce-cli.exe on Windows). They differ in how the value is rendered into your template:

  • {{sidecar_path}} emits the path verbatim. Right for bare argv slots, env-var values, or anywhere the surrounding parser doesn't have its own string-escape grammar.
  • {{sidecar_path_jsonsafe}} emits the path with \\\ and "\" applied. Use this when the placeholder lands inside a JSON or TOML "..." string — which is how every bundled per-session MCP config wires up Spruce:

yaml

modes:
  claude:
    binary: claude
    args:
      - --mcp-config
      - '{"mcpServers":{"spruce":{"command":"{{sidecar_path_jsonsafe}}","args":["mcp","serve"]}}}'

Why the distinction matters: a Windows sidecar path looks like C:\Users\me\spruce-cli.exe, and the raw form embedded inside a JSON string produces \U, \A, etc. — none of which are valid JSON escape sequences. Claude (and Codex, OpenCode, Gemini) then silently fall back to treating the argument as a file path. The _json form solves this by escaping at substitution time.

Note that {{sidecar_path_jsonsafe}} provides only the interior escape — keep the surrounding "..." in your template; the placeholder does not add its own quotes.

If the bundled sidecar can't be resolved (e.g. a dev build), both placeholders fall back to the string "spruce", so the snippet still works as long as spruce-cli is on PATH.

How {{session_file:<name>}} works

Some agents only read MCP config from a settings file rather than a CLI flag. For those, a mode can declare a session_files: map whose contents Spruce writes to disk per session — under <tmp>/spruce-sessions/<session_id>/<name> — and {{session_file:<name>}} resolves to that file's path. This is how the bundled Gemini mode wires up MCP:

yaml

modes:
  gemini:
    binary: gemini
    args:
      - --prompt-interactive
      - "{{action_content}} Get artifact {{artifact_id}} and begin."
    session_files:
      gemini_settings:
        content: '{"mcpServers":{"spruce":{"command":"{{sidecar_path_jsonsafe}}","args":["mcp","serve"]}}}'
    env:
      GEMINI_CLI_SYSTEM_SETTINGS_PATH: "{{session_file:gemini_settings}}"

Each session file's content: is itself run through {{placeholder}} substitution before being written, so {{sidecar_path_jsonsafe}} inside a session-file body resolves the same way it does anywhere else (still JSON-escaped, since the surrounding context is JSON).

How {{context:<name>}} works

Spruce ships a few canned context blocks that explain its primitives to the agent: {{context:artifact-system}} describes the artifact model, {{context:comment-system}} describes the comment system, and so on. These get inlined into the prompt at runtime so the agent has the relevant background without bloating every action's body.

The block names map to files Spruce maintains; you don't define them yourself.

Example: agent action

markdown

---
display:
  icon: list-tree
modes:
  claude:
    binary: claude
    args:
      - --mcp-config
      - '{"mcpServers":{"spruce":{"command":"{{sidecar_path_jsonsafe}}","args":["mcp","serve"]}}}'
      - --system-prompt
      - "{{action_content}}"
      - --allowedTools
      - "Read,Glob,Grep,Edit,Write,Bash,mcp__spruce__*"
      - --
      - "Get artifact {{artifact_id}} and begin."
---
You are a software architect.

Current date: {{current_time}}
Session ID: {{session_id}}

{{context:artifact-system}}
{{context:comment-system}}

When run on artifact SPR-abc123 with session sess-42, the agent ends up with the body fully substituted, {{sidecar_path_jsonsafe}} pointing at the bundled CLI (with backslashes escaped for the surrounding JSON string), and {{artifact_id}} already inlined into the user-turn prompt.

Example: non-agent action

markdown

---
display:
  icon: target
modes:
  shell:
    command: npm test -- --grep "{{artifact_id}}"
---

Run on artifact SPR-abc123 and the command becomes npm test -- --grep "SPR-abc123".

Paths and the worktree

{{code_path}} points to the worktree for the current artifact when one exists (i.e. the artifact has been Start'd). Otherwise it points to the main repo checkout. This is what you want most of the time: actions running against an in-flight artifact stay isolated to that artifact's worktree.