Skip to content

Macro template rendering

This page documents template rendering, placeholders, protected regions, and expansion.

Jinja2 Integration

When the template body contains Jinja2 markers ({{ }}, {% %}, or {# #}), it is rendered as a Jinja2 template. Arguments (both positional and named) are available in the template context.

---
input: { user: word, verbose: { type: bool, default: false } }
---

Hello, {{ user }}.

{% if verbose %} Here is the detailed explanation... {% endif %}

Template Context

Variable Description
{{ name }} Named argument or input mapped by name
{{ _1 }} First positional argument (1-indexed)
{{ _2 }} Second positional argument, etc.
{{ _args }} List of all positional arguments
{{ root }} Absolute path to the primary workspace directory (omitted if unresolvable)
{{ wait.chats }} List of chat-transcript paths for agents named in %wait:<name> directives, in the order they appear
{{ wait.artifacts }} Lazy list of non-chat artifact metadata dictionaries produced by those waited agents; no file contents
{{ agents["build"].path }} Output variables loaded from %wait:build when that agent used sase var set path=...
{{ agents["p--plan"].plan_file }} Proposed plan path of a submitted planner row, synthesized from %wait:p--plan (no sase var set needed)
{{ agents["planner"].created_epic }} First epic a waited target launched, synthesized from %wait:planner (no sase var set needed)
{{ agents["planner"].created_epics }} Every epic a waited target launched, synthesized from %wait:planner (no sase var set needed)
{{ patch_name }} Name of the patch the agent run works on
{{ workspace_num }} 1-based number of the workspace directory assigned to the agent run
{{ n }} / {{ N }} Current %repeat iteration (1-based) and total iteration count
{{ provider_name }} Name of the LLM provider rendering a skill file (skill frontmatter only)
{{ provider_tool_name }} Display name of the provider tool rendering a skill file (skill frontmatter only)
{{ provider_native_ask_tool }} Name of the provider's native ask-user-question tool (skill frontmatter only)
{{ range(...) }} Jinja globals: range, dict, lipsum, cycler, joiner, namespace

Accepted template aliases are listed in Macro migration notes.

Named arguments and positional-to-name mappings take priority; if a macro is called within a workflow step, the workflow's execution scope is also available (macro args override scope values on conflict).

The wait namespace is only populated while an agent run is rendering its executable prompt. wait.artifacts is evaluated on first access and returns plain dictionaries with metadata such as wait_name, agent_name, ref, kind, label, path, source_path, and nullable VCS provenance fields. Read artifact contents explicitly with sase artifact read <ref> "<reason>" when the prompt needs bytes.

A macro that needs runtime-only names such as wait.artifacts defers them with {% raw %}...{% endraw %}, and the deferred template renders in the launch-time top-level pass, where fenced and inline code are literal. A {{ ... }} inside backticks there is never substituted, while an enclosing {% for %} still repeats the literal once per item, so deferred expressions must stay outside code spans. Rendered values still go through literal-preserving prompt formatting, so __ and * in paths survive.

Availability: conditional names render only when their precondition holds — n/N need %repeat, agents/wait_chats need %wait, and the provider_* names need truthy skill frontmatter in macro scope. A prompt that declares its own input: frontmatter renders before any agent run exists, so the run-time names above (wait, patch_name, workspace_num, cl_name, n, N, agents, wait_chats) are unavailable there. Typing {{ in the prompt input or an editor with the macro LSP offers exactly the names that render in the current scope; see Editor LSP.

Filters

Filter Description
{{ plan_file \| plan_ref_path }} Returns the YYYYmm/<name>.md portion of a plan path or plan: reference; passes non-plan values through unchanged
{{ "grok" \| provider_disabled }} true when the named LLM provider has an active machine-wide disable; false otherwise
{{ "grok" \| provider_enabled }} true when the named LLM provider has no active machine-wide disable; false otherwise

Both provider filters accept an optional mode argument — "any" (the default), "hard", or "soft" — so {{ "grok" \| provider_disabled("soft") }} is true only for an active soft disable. A non-string, blank, or unknown provider name returns not-disabled rather than raising, so a templating slip cannot claim a provider is down; a corrupt disable-state file likewise fails open. An unknown mode raises ValueError so the typo fails loudly at render time.

The positive form exists for static conditional segments, where the condition must read as a single should_run= value:

Always launch this segment.
---
%if(should_run={{ "grok" | provider_enabled }})
This segment is dropped whole whenever grok is disabled.

Raw Prompt Placeholders

sase's TUI recognizes valid single-line <label> tags as raw placeholders. A label must be nonempty, contain no leading or trailing whitespace, and be at most 100 characters. Raw placeholders are highlighted in prompt panes, the prompt-stash, trash, and pinned-stash previews, and the agent panel's raw-prompt header. They participate in placeholder completion and feed the saved common-placeholder history described in sase's TUI completion. Repeated tags with the same exact, case-sensitive inner text are one logical placeholder. Tags inside inline code, fenced code blocks, or %macros_enabled:false regions stay literal and are excluded from highlighting, completion history, launch-time collection, and conversion.

Classification is syntactic rather than HTML-aware: <div>, </div>, and an angle-bracket link destination outside a code zone are placeholders too, and a preceding backslash does not escape them. Keep literal angle-bracket markup in an inline or fenced code zone, place it in a disabled macro region, or use the launch panel's keep-literal control.

By default, submitting a prompt from sase's TUI opens Fill in this prompt whenever the prompt body contains a live raw placeholder. The panel shows raw placeholders first and then any frontmatter-declared inputs, so both kinds are resolved before the agents launch. Enter one value per distinct label to replace every matching occurrence across all segments, or press Ctrl+L on a placeholder field to keep that tag literal. YAML frontmatter itself is not scanned. Set ace.prompt_inputs.collect_raw_placeholders: false to skip only raw-placeholder collection and launch the tags unchanged; declared input: values are still collected. Non-interactive sase run does not collect raw placeholders.

When an sase's TUI draft is saved through the whole-stack macro flow (gX or Ctrl+G X in macro mode), live raw placeholders are converted before the save preview into required text inputs:

Deploy <service> to <target file>

becomes:

---
input:
  service: text
  target_file: text
---

Deploy {{ service }} to {{ target_file }}

The gL active-pane conversion applies the same rewrite when it creates a frontmatter-local macro. A fresh gx mini-macro extraction applies it to the copied origin-pane body before the mini pane opens and seeds the mini definition's inferred inputs. Raw placeholders typed later in the mini pane are saved as edited; the mini save review does not run another conversion pass. Generated names are Jinja-safe slugs allocated in document order; collisions receive _2, _3, and so on. During macro saves, a generated name that matches an authored input is reused instead of redeclared, preserving its type, default, and description. All conversion paths reuse a matching undeclared Jinja variable. Repeated occurrences are substituted together, tags in literal zones remain untouched, and inserted values are not scanned again for more placeholders. Saving the same draft as a snippet keeps the original active-pane body rather than applying this macro-only conversion. Writing an already bound macro with gw saves the body as edited and does not perform a new conversion pass.

Set ace.prompt_inputs.macro_placeholder_args: false to keep live raw placeholders literal during gX, gL, and fresh gx extraction and mint no placeholder-derived text inputs. Undeclared Jinja variables still become required gL inputs.

Command Substitution

Macro arguments support shell command substitution using $(cmd) syntax. The command is executed via the shell and its output replaces the $(cmd) expression.

#bug:$(branch_bug)           # Use output of branch_bug command as the argument
#review:$(git diff HEAD~1)   # Pass git diff output as argument

Nested parentheses are supported: $(echo $(date)). To include a literal $(, escape it as \$(.

Failed commands or commands producing empty output result in an empty string replacement. Command outputs are cached within a single expansion pass to avoid redundant execution.

Protected Content

Fenced Code Blocks

Content inside triple-backtick fenced code blocks is automatically protected from macro expansion:

Here's an example:

```
#foo will NOT be expanded inside this code block
```

But #foo HERE will be expanded normally.

This prevents accidental expansion of #name patterns in code examples, documentation, and similar content. The %if:: and %proc:: forms are the exception: the immediately following bash or python fence belongs to that directive, so its content is captured as opaque structured code and the directive plus fence are stripped from the model prompt. See Typed launch units.

Disabled Regions

You can explicitly disable macro expansion for a region of text using the %macros_enabled directive:

%macros_enabled:false
This content is passed through verbatim.
#foo will NOT be expanded here.
%macros_enabled:true
Normal expansion resumes here.
#foo WILL be expanded.

The markers are stripped from the final output. This is useful for embedding raw macro syntax in documentation or for passing literal #name patterns to downstream consumers.

The closing %macros_enabled:true marker may appear either on its own line or inline at the end of a content line. In both forms the marker (and any whitespace immediately preceding an inline marker) is stripped from the final output, so prompts authored as natural prose can re-enable expansion mid-line:

%macros_enabled:false
... raw content where #foo and @bar are passed through verbatim. %macros_enabled:true
And expansion resumes here.

Macro Aliases

Macro aliases provide raw text-level substitution that runs before any other macro processing. They are defined in the macro_aliases config field in sase.yml.

These are global shorthand aliases for macro names and raw refs. They are separate from ProjectSpec PROJECT_NAME and PROJECT_ALIASES, which map friendly project refs such as bob to canonical directory-key projects such as gh_bbugyi200__bob at the launch boundary. Project names and aliases are canonicalized before macro expansion so launch artifacts and history store the canonical directory-key project name.

The built-in defaults provide two shorthand aliases:

Alias Target Usage
c commit #c → #commit
p propose #p → #propose

Additional aliases can be added in user config files:

macro_aliases:
  deploy_notes: "release-notes" # #deploy_notes → #release-notes
  gh_foo: "gh:foo/bar" # #gh_foo  → #gh:foo/bar

When the processor encounters #alias_name in a prompt, it replaces the alias name portion with the target string before any macro resolution occurs. This is particularly useful when the target contains characters (like :) that must be present in the raw text for other processing logic — such as VCS directory-switching — to work correctly.

See Configuration Reference: macro_aliases for the full field specification.

Recursive Expansion

Macro bodies can reference other macros. Expansion is iterative: after each round of substitution, the result is scanned again for new #name references. This continues until no known references remain, up to a maximum of 100 iterations (to guard against circular references).

_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.