Skip to content

XPrompt Template Reference

XPrompts are reusable prompt templates with optional typed inputs and Jinja2 support. They let you define a prompt fragment once and reference it by name anywhere a prompt is composed, keeping prompts DRY and consistent across projects. Inline prompt fragments use #name; standalone workflows use #!name when they are launched as workflows.

Use xprompts when you want to:

  • Share common instructions across multiple prompts (e.g., output format rules, role definitions).
  • Parameterize prompts with typed, validated arguments.
  • Compose prompts from smaller building blocks using #name(args) syntax.

There are two related paths to keep separate:

launch setup:
  multi-agent xprompt fan-out check
  -> default workspace ref insertion when needed (#git:home)
  -> project alias canonicalization (#gh:bob -> #gh:bob-cli)
  -> workspace ref resolution (#cd/#git/#gh/#hg and known-project fallbacks)
  -> prompt/workflow execution

xprompt expansion inside a prompt or prompt_part:
  alias substitution
  -> fenced-block and disabled-region protection
  -> iterative reference expansion (parse -> lookup -> args -> render -> substitute)
  -> directive extraction at the launch or workflow-step boundary

The checked-in infographic prompt in docs/images/xprompt-resolution-infographic.prompt.md tracks the intended visual version of this model; the text model above is the authoritative current reference for resolver order.

Table of Contents

CLI Subcommands

The sase xprompt command provides five subcommands for working with xprompts. With no subcommand, it defaults to sase xprompt list. Flags belong to the explicit subcommand, so use forms like sase xprompt expand --trace '#plan' rather than putting --trace on bare sase xprompt.

sase xprompt expand

Expands xprompt references in a prompt. Reads from a positional argument or stdin.

sase xprompt expand '#greet(Alice)'         # Expand from argument
echo '#greet(Alice)' | sase xprompt expand  # Expand from stdin
sase xprompt expand --trace '#plan'         # Show expansion trace on stderr

The --trace flag prints a detailed expansion trace to stderr showing each resolved reference, its source file, arguments, and expanded content. This is useful for debugging reference resolution order and understanding how a complex prompt is assembled.

sase xprompt explain

Shows a dry-run visualization of a workflow's execution plan without actually running it. Displays workflow metadata, input requirements, resolved arguments, and the full step-by-step execution plan with types, control flow annotations, rendered step bodies, and output schemas.

sase xprompt explain my_workflow                    # Explain with no args
sase xprompt explain my_workflow arg1 arg2          # With positional args
sase xprompt explain my_workflow --arg key=value    # With named args

sase xprompt list

Lists all available xprompts and workflows as a JSON array. Each entry includes the name, type ("xprompt" or "workflow"), kind, reference prefix, insertion text, is_skill, source file path, user-facing input definitions, tags, and a content preview. Clients should treat insertion as the authoritative reference text. Most xprompt and embeddable_workflow entries insert as #name, including markdown multi-agent xprompts; standalone workflows insert as #!name. is_skill is true only for xprompt catalog entries marked as skills; workflows report false. Step inputs are omitted from the JSON inputs array because they are supplied by workflow execution rather than typed by a user.

sase xprompt list                   # JSON array to stdout
sase xprompt list | jq '.[].name'  # Extract just names

sase xprompt graph

Generates a directed acyclic graph (DAG) visualization of a workflow. Without a workflow name, lists all available multi-step workflows with their step counts and source paths.

sase xprompt graph                        # List all workflows
sase xprompt graph my_workflow            # Mermaid DAG (default)
sase xprompt graph my_workflow --format text  # Plain-text summary

The Mermaid output can be pasted into any Mermaid-compatible renderer. Parallel sub-steps are shown as subgraphs, and nodes include type indicators and control flow annotations.

sase xprompt catalog

Renders every visible xprompt to a formatted PDF catalog for browsing and sharing.

sase xprompt catalog                # Write the PDF to a tempdir and print its path
sase xprompt catalog --out /tmp/out # Write the PDF to the specified directory

The command collects all visible xprompt templates, renders each into an HTML section, and produces a single PDF using the bundled catalog_template.html.j2 and catalog_style.css. The mobile/helper structured catalog uses the same collection and classification code, but returns JSON metadata instead of requiring a PDF renderer.

Editor LSP

sase lsp starts the SASE xprompt language server over stdio for editor integrations. It resolves the server command in this order:

  1. SASE_XPROMPT_LSP_CMD, parsed as a shell-style command for development.
  2. sase-xprompt-lsp on PATH.
  3. A debug or release sase-xprompt-lsp binary under a sibling ../sase-core checkout.
  4. cargo run --manifest-path ../sase-core/Cargo.toml -p sase_xprompt_lsp -- when cargo is available and the sibling checkout has a Cargo.toml.

Examples:

sase lsp
sase lsp --version
SASE_XPROMPT_LSP_CMD='cargo run --manifest-path ../sase-core/Cargo.toml -p sase_xprompt_lsp --' sase lsp

Use SASE_XPROMPT_LSP_CMD for any non-default LSP command. SASE_CORE_DIR is a Justfile build/install override, not part of sase lsp command resolution.

The LSP loads the supported xprompt catalog sources directly in Rust for completion, hover, diagnostics, and definition requests. sase lsp exports the installed package xprompt paths to the server so built-in Markdown prompts, YAML workflows, default config prompts, project-local prompts, user config prompts, and memory prompts do not require a Python helper subprocess on the completion path. The Python helper bridge remains stable for mobile clients and as a compatibility fallback for sources the Rust loader cannot discover.

When the editor advertises LSP completionItem.snippetSupport, the server also returns SASE snippets as ordinary CompletionItemKind.Snippet entries after bare trigger words such as fix or review. Snippet entries are loaded from the same registry as ACE: xprompts with snippet front matter plus user-defined ace.snippets, with ace.snippets winning on trigger collisions. The editor does not need to shell out or parse SASE config to discover snippets.

The Python helper operation sase editor helper-bridge snippet-catalog is the authoritative snippet registry because it matches ACE's xprompt composition behavior. The Rust server also has a native fallback for simple xprompt snippets and ace.snippets so completion can degrade gracefully if the helper is unavailable. That fallback intentionally skips xprompts that require complex Jinja or composition it cannot mirror exactly; when the helper is available, its response is preferred.

See the editor integration guide for setup, feature coverage, helper bridge usage, and troubleshooting.

Discovery Order

Markdown xprompts are loaded from multiple locations. When two locations define an xprompt with the same name, the higher-priority source wins (first-wins).

Priority Location Notes
1 .xprompts/ (CWD, hidden dir) Highest priority; project-local overrides
2 xprompts/ (CWD) Non-hidden variant
3 ~/.xprompts/ (home, hidden dir) User-wide overrides
4 ~/xprompts/ (home) Non-hidden variant
5 ~/.config/sase/xprompts/{project}/ Project-specific (when project is set)
6 sase.yml xprompts: section Config-based definitions (local + global)
7 Plugin packages (sase_xprompts EPs) Installed plugin xprompts
8 <sase_package>/default_xprompts/*.md Built-in default markdown xprompts
9 <sase_package>/xprompts/*.md Built-in package xprompts shipped with core SASE

Each directory-based source can contain individual .md files. YAML workflows use the same CWD, home, project, plugin, and package locations, with .yml or .yaml files loaded as workflow definitions. Within priority 6, the config merge chain applies: built-in defaults, plugin configs, ~/.config/sase/sase.yml, overlay files (sase_*.yml), and finally a local ./sase.yml in the current working directory (highest config priority).

For file-based xprompts (priorities 1-5 and 7-9), the xprompt name defaults to the filename stem (e.g., summarize.md defines the xprompt summarize). The name can be overridden via the name field in the YAML front matter.

Project-specific xprompts (priority 5) are namespaced: a file bar.md in the foo project directory becomes foo/bar. Inline-capable project xprompts are referenced as #foo/bar; standalone project workflows are referenced as #!foo/bar.

When a project is detected (via the workspace provider), CWD xprompts (priorities 1-2) and local config xprompts are also auto-namespaced with the {project}/ prefix. For example, if the project is myapp and xprompts/deploy.md exists in the CWD, it becomes myapp/deploy and is referenced as #myapp/deploy. A project workflow with no prompt_part would instead be launched as #!myapp/deploy. This prevents name collisions between project-local xprompts and global or built-in ones.

File Format

An xprompt file is a Markdown file with optional YAML front matter delimited by --- lines. Everything after the closing --- is the template body.

---
name: greet
description: Greet a named user.
input:
  user_name:
    type: word
    description: User name to include in the greeting.
---

Hello, {{ user_name }}! Welcome aboard.

Front Matter Fields

Field Required Description
name No XPrompt name (defaults to filename stem)
input No Input parameter definitions (see Typed Inputs)
snippet No Opt-in to ACE snippet expansion (see Snippet Field below)
description No Human-readable one-line description of what the xprompt does
skill No Marks this xprompt as an agent skill source for sase skill init (see below)
xprompts No File-local helper xprompts whose names must start with _

If no front matter is present, the entire file content is the template body and the filename stem is the name.

Markdown xprompt files can carry file-local helper xprompts under xprompts:. These helpers use the same structured format as config-based xprompts, including typed inputs and descriptions, and they can reference each other transitively. During expansion they inherit the containing xprompt's arguments and template scope, so a helper can use values such as {{ topic }} from the outer xprompt. They are visible only while expanding the containing xprompt and must use _-prefixed names such as _review_rules; they do not leak into the global catalog, completion catalog, or other xprompt files. This underscore rule also applies to local xprompts in ad hoc prompt front matter, while YAML workflow-local xprompts follow the workflow rules described in workflow_spec.md.

Reference Syntax

Reference inline-capable xprompts inside any prompt with the # prefix, including markdown-defined multi-agent xprompts whose body contains top-level --- segment separators. Use #! only for standalone YAML workflows that do not have a prompt_part step. The marker must appear at the start of the string, after whitespace, or after one of ([{"'. For compatibility, #!name is still accepted for multi-agent xprompts, but new prompts should use #name.

Syntax Description
#name Inline/template reference, no arguments
#name(args) Inline parenthesis syntax with comma-separated arguments
#name:arg Inline colon syntax, passes arg as a single positional arg
#name:a,b,c Inline colon syntax with comma-separated multiple args
#name:`arg with spaces` Colon+backtick syntax for args containing spaces (single only)
#name+ Plus syntax, equivalent to #name:true
#ns/name Namespaced reference (e.g., project-specific)
#!name Standalone workflow reference, no args
#!name(args) Standalone workflow reference with parenthesized arguments
#!name:arg Standalone workflow reference with one colon-style arg
#!name!! / #!name?? Standalone workflow with an explicit HITL approval override

Examples:

sase run '#!sync'
sase run '#gh:sase #!sase/pylimit_split %approve'

During the compatibility window, top-level legacy invocations such as sase run '#sync' still run but emit a warning that points to #!sync. Inline expansion contexts reject standalone workflows instead of passing literal #sync text to the model. Shell examples should use single quotes around #!... so ! is not interpreted by interactive shells.

For workspace references, underscores can be used as an alternative to colons: #gh_sase is equivalent to #gh:sase. The underscore is normalized to a colon before pattern matching, so both forms work identically. This is useful in contexts where colons are inconvenient. The #cd directory workflow is still a workspace reference even though it skips VCS checkout/release work.

Provider-backed references also support @name agent references in the ref portion. The @name is resolved at runtime to the named agent's ChangeSpec (branch name), allowing one agent's prompt to target another agent's workspace:

#gh:@planner     resolves to e.g. #gh:planner_add_config_parser
#gh_@reviewer    same, underscore form

This is useful when chaining agents — for example, a review agent can target the branch created by a prior agent using @name instead of hardcoding the branch name.

VCS Workspace References

Workspace-managing workflows use the same #name:ref reference syntax as xprompts, but they control where the agent runs before the rest of the prompt is executed. Some workspace references are VCS-backed (#git, #gh, #hg); #cd is directory-backed and does not reserve a numbered workspace.

Reference Behavior
#cd:<path> Run in a local directory without reserving a numbered SASE workspace or doing VCS checkout/release work
#git:<ref> Run in a bare-git workspace
#gh:<ref> Run in a GitHub workspace, when the GitHub plugin is installed
#hg:<ref> Run in a Mercurial workspace, when an hg workspace plugin is installed

Prompts that do not contain a workspace reference are normalized to #git:home, so a bare prompt runs from the managed bare-git home project by default and gets normal numbered workspace, checkout, diff, and release behavior. Use #cd:~, #cd:/abs/path, #cd:relative/path, #cd:../sibling, or #cd(.) to choose a directory explicitly and skip VCS workspace management.

By default, a missing or uninitialized home ProjectSpec is bootstrapped as a managed empty bare-git project at the default home paths. To make bare prompts use an existing home/dotfiles bare repository, register a bare repository whose basename resolves to home, for example #git:/path/to/home.git. Use #cd:~ when you want a direct home-directory run without VCS workspace management.

Provider-prefixed refs that point at a known project name are preserved as workspace launches even if the matching workspace plugin is not loaded in the current process. Known projects come from ~/.sase/projects/*/*.sase (with legacy ~/.sase/projects/*/*.gp accepted as a fallback). A launch such as #gh:sase #!fix_just therefore targets the registered sase project, allocates a numbered workspace for non-wait runs, and lets dispatch surfaces strip the wrapper ref when identifying an embedded workflow body.

Known projects may also declare PROJECT_ALIASES in their ProjectSpec. Alias refs in VCS workspace tags are canonicalized before workspace resolution and xprompt expansion, so #gh:bob #p is processed as #gh:bob-cli #p when the bob-cli project declares alias bob. The rewrite is exact and applies to colon, underscore, and parenthesized workspace-ref forms; it does not rewrite owner/repo paths such as #gh:bbugyi200/bob, partial project names, prose, or fenced code examples. See Project Aliases for validation and management commands.

GitHub owner/repo refs use aliases after first use. Resolving #gh:foo-org/foo creates or reuses the canonical project whose WORKSPACE_DIR is ~/projects/github/foo-org/foo/; for a new repo that canonical name is typically gh_foo-org__foo, with generated alias foo. A second repo with the same basename, such as #gh:bar-org/foo, gets a different canonical project such as gh_bar-org__foo and the next available alias, for example foo-2. Future launches can use #gh:foo and #gh:foo-2, and those refs canonicalize before prompt history, metadata, and artifacts are written.

For compatibility, existing basename ProjectSpecs are reused when their WORKSPACE_DIR already matches the GitHub repo. Owner/repo fallback avoids basename routing when duplicate GitHub basenames would make that ambiguous; direct owner/repo refs match the GitHub workspace path first, then only use a basename fallback when it is unambiguous.

ACE and the xprompt LSP provide a project/ChangeSpec completion helper for these references. Type #+ at a token boundary, or type + as the first character in the prompt, to open a picker of active launchable projects and active PR-sized ChangeSpecs in WIP, Draft, Ready, or Mailed status. Accepting a project row inserts a tag such as #gh:sase; accepting a ChangeSpec row inserts a tag such as #gh:my_change. The helper filters by project name, project alias, or ChangeSpec name prefix, and it ignores system-managed home, inactive projects, sibling records, and non-launchable projects.

Known-project lookup defaults to active ProjectSpecs. Inactive and sibling projects are omitted from broad project-local xprompt catalogs and normal VCS workspace resolution; an explicit reference to an inactive known project fails with a hint to run sase project activate <project> before launching new work. Management and history code paths that need hidden projects opt into an all-state scan explicitly.

The raw colon form stops at whitespace, so paths with spaces should use the parenthesized form when possible: #cd(/tmp/my project). Backtick quoting is supported for ordinary xprompt arguments, but workspace-reference path matching is intentionally conservative; prefer paths without spaces for embedded workspace tags.

Double underscores (__) in xprompt names are treated as forward slashes (/), enabling flat references to namespaced xprompts. For example, #foo__bar resolves to the xprompt registered as foo/bar, and #a__b__c resolves to a/b/c. Single underscores are not affected. This is useful when / is inconvenient in certain input contexts (e.g., shell completion or certain prompt editors).

Markdown headings like # Heading are not matched because a space after # prevents the pattern from firing.

Arguments

Positional Arguments

Positional arguments are comma-separated values inside parentheses:

#greet(Alice)
#format(json, 4)

Positional arguments are mapped to input definitions by position (0-indexed).

Named Arguments

Named arguments use key=value syntax:

#greet(user_name=Alice)
#format(style=json, indent=4)

Positional and named arguments can be mixed; positional arguments must come first:

#template(Alice, style=formal)

Quoted Strings

Values containing commas or special characters can be double- or single-quoted:

#note("Hello, world!", priority=high)
#tag('key=value')

Text Blocks

For multi-line argument values, use [[...]] delimiters:

#review([[
  This is a multi-line
  text block argument.
  Blank lines are preserved.
]])

Text blocks automatically strip leading whitespace from the first line and dedent continuation lines by their minimum common indentation.

Shorthand Syntax

Shorthand syntax converts line-oriented prompt text into #name([[text]]) calls, avoiding the need for explicit text block delimiters.

Single-Colon Shorthand

#name: text at the start of a line captures text until a blank line (\n\n) or end of string:

#review: Please check this code for correctness
and performance issues.

This is equivalent to #review([[Please check this code for correctness\nand performance issues.]]).

Double-Colon Shorthand

#name:: text captures text until the next xprompt directive at a line boundary or end of string (blank lines do not terminate it):

#instructions:: Follow these rules:

1. Be concise
2. Be accurate

#review: Now review the code.

Paren + Shorthand

Combine parenthesized args with shorthand text:

#template(style=formal): Please review the following code.
#template(style=formal):: Please review the following code.

Even across blank lines (double-colon only).

The text is appended as a final positional text-block argument.

Typed Inputs

XPrompts can declare typed input parameters in the YAML front matter.

Longform Syntax

input:
  - name: diff_path
    type: path
    description: Diff file to review.
  - name: max_retries
    type: int
    default: 3
    description: Maximum retry attempts.

Shortform Syntax

input:
  diff_path: path
  max_retries:
    type: int
    default: 3
    description: Maximum retry attempts.

Both forms accept optional one-line description fields. Input descriptions do not change argument parsing or compact input signatures; rich surfaces such as catalogs, explain output, argument help, and editor documentation can use them as human-facing help text.

Supported Types

Type Aliases Validation
word -- No whitespace allowed
line -- No newlines allowed (default type)
text -- Any content, no restrictions
path -- No whitespace
int integer Must parse as an integer
bool boolean Accepts true/false, yes/no, 1/0, on/off
float -- Must parse as a float

Defaults

  • An input with no default is required. Omitting it causes a template error if the caller does not supply a value.
  • default: null means the YAML value was explicitly null. When null is passed as a positional or named argument value, it acts as a pass-through (the callee's own default applies).
  • default: "" or any other value makes the input optional with that default.

Output Specification

XPrompts used as agent steps in workflows can declare an output schema for structured output validation. See the Output Specification section in the workflow spec for full details on the format.

Shortform Object

output: { name: word, description: text }

Shortform Array

output: [{ name: word, description: text, parent: { type: word, default: "" } }]

Longform

output:
  type: json_schema
  schema:
    properties:
      name: { type: word }
      description: { type: text }

When an output spec is present, the agent's response is validated against the schema. Semantic types (word, line, text, path, bool, int, float) are converted to JSON Schema types for validation and then checked for additional constraints (e.g., word rejects whitespace).

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
{{ agents["build"].path }} Output variables loaded from %wait:build when that agent used sase var set path=...

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

Legacy Placeholders

For templates that do not use Jinja2 syntax, a legacy placeholder mode is available. Placeholders use {N} syntax (1-indexed):

Review the {1} module and check for {2:correctness}.
  • {1} -- required first positional argument.
  • {2:correctness} -- second positional argument with default correctness.

Legacy mode is auto-detected: if the body contains no Jinja2 markers, legacy substitution is used.

Tags

XPrompts and workflows can be annotated with semantic role tags. Tags enable lookup-by-role instead of lookup-by-name, making the system extensible — a plugin or user can override the CRS workflow simply by defining a new xprompt with tags: crs.

Available Tags

Tag Description
vcs Workspace workflow xprompt (#cd, #git, #gh, #hg) — wraps other embedded workflows, running setup/teardown around them
crs Code Review Summary workflow (singleton — get_by_tag(crs) returns the first match)
fix_hook Fix hook workflow (singleton — used by axe to find the hook-fix agent)
rollover Marks workflows whose embedded references carry forward to follow-up agent steps
mentor Mentor review prompt workflow
commit Commit workflow (appended by mentor review A key for direct commit)
propose Propose workflow (appended by mentor review a key for propose-style amend)
make_mentor_changes Apply accepted mentor comments workflow (launched by mentor review Enter)
diff_file Injects the CL diff into the mentor prompt
append_to_pr VCS-specific post-commit prompt appended when the active commit method creates a pull request
append_to_commit_and_propose VCS-specific post-commit prompt appended when the active commit method creates a commit or proposal
create_epic_bead Plan-approval Epic flow — creates the plan file, beads, and the epic agent prompt
create_legend_bead Plan-approval Legend flow — creates a legend-tier bead with epic_count, then runs legend work
work_phase_bead Per-phase agent prompt used by sase bead work (input: bead_id)
land_epic Final land-the-epic agent prompt used by sase bead work after all phases complete
land_legend Final land-the-legend agent prompt used by sase bead work after legend epics complete

Defining Tags

Tags can be defined in three places:

YAML workflow files (.yml):

tags: vcs, rollover       # comma-separated string
# or
tags: [vcs, rollover]     # list format

Markdown front matter (.md):

---
name: fix_hook
tags: fix_hook
---

Fix the failing hook...

Config-based xprompts (sase.yml):

xprompts:
  my_crs:
    content: "Review the code..."
    tags: [crs]

Tag-Based Lookup

The get_by_tag() function returns the first xprompt/workflow matching a tag, respecting the standard discovery order. This means higher-priority sources (e.g., project-local) can override built-in tagged xprompts.

from sase.xprompt.tags import XPromptTag, get_by_tag

crs_wf = get_by_tag(XPromptTag.crs)
fh_wf = get_by_tag(XPromptTag.fix_hook)

Backward Compatibility

The legacy wraps_all: true field on workflows is still supported — it automatically adds the vcs tag. New workflows should use tags: vcs instead.

Source: src/sase/xprompt/tags.py, src/sase/xprompt/models.py

Snippet Field

XPrompts can opt-in to ACE TUI snippet expansion by setting the snippet field in their front matter. When set, the xprompt's content is converted into a snippet template and merged into the ACE snippet registry at startup, so users can expand it by typing the trigger word and pressing Tab.

---
name: review
snippet: true
input:
  language: word
---

Review this {{ language }} code for correctness and style.

Values:

Value Behavior
true Use the xprompt's base name (part after last /) as trigger
"custom_name" Use the custom string as the trigger word

Conversion rules:

  • Normal xprompt references in the content are expanded before conversion, so snippets can compose reusable xprompts
  • {{ input_name }} placeholders for required inputs become snippet tabstops ($1, $2, etc.)
  • {{ input_name }} placeholders for inputs with defaults are pre-filled with the default value
  • Legacy {N} placeholders are also converted
  • XPrompts with complex Jinja2 control flow ({% %} or {# #}) are skipped
  • User-defined snippets in ace.snippets take precedence over xprompt-derived snippets on name collision

Snippet templates can reuse other snippets with #[trigger] after the xprompt snippets and ace.snippets entries are merged. The referenced snippet's $1, $2, ... tabstops are spliced into the caller and renumbered in document order:

ace:
  snippets:
    greet: "Hello $1!$0"
    welcome: "#[greet] Welcome to $1.$0"

welcome expands as Hello $1! Welcome to $2.$0. Positional arguments fill the referenced tabstops before the splice: #[greet(World)] or #[greet:World] expands as Hello World!.

Editor clients receive the same templates through sase lsp when they support LSP snippets. To troubleshoot the raw registry, run:

printf '{"schema_version":1}\n' | sase editor helper-bridge snippet-catalog

See docs/ace.md — Snippets for snippet usage in the prompt input widget and editor completion.

Source: src/sase/xprompt/snippet_bridge.py, src/sase/xprompt/models.py

Skill Field

XPrompts can be marked as agent skill sources by setting the skill field in their front matter. sase skill list shows the loaded skill catalog without writing files. sase skill init reads that catalog, including bundled skill sources and runtime config overlays, to determine which xprompts should be rendered into per-provider SKILL.md files and deployed to agent skill directories. By default, generated skill files begin with a sase skill use <name> --reason ... directive so SASE can audit which skills an agent used; set log_skill_use: false in a skill source to omit that directive (see below). Recorded skill uses can be summarized and inspected with sase skill log. The compatibility alias sase init skills runs the same initializer.

---
name: sase_git_commit
skill: true
description: Commit changes using sase commit for git-based VCS
---

Commit instructions here...

Values:

Value Behavior
true Deploy to all registered providers
["claude", "agy"] Deploy only to the listed providers

The description field provides a human-readable summary shown in sase xprompt list and sase skill list output. The structured catalog also marks these entries with is_skill: true; ACE and editor clients use that flag to offer slash-skill completions such as /sase_plan while keeping ordinary xprompts out of slash completion results.

The optional log_skill_use boolean field controls the generated audit directive. It defaults to true, so generated skills instruct the agent to run sase skill use <name> --reason ... as their first step. Set log_skill_use: false to suppress that directive for skills that should not record their own use (the bundled /sase_plan and /sase_memory_read skills set this). The field only affects sources that are also marked as skills.

Workflow: Edit packaged skill sources in src/sase/xprompts/skills/, or define user/runtime skill xprompts through the normal xprompt catalog sources. Do not include the sase skill use directive yourself; the generator injects it unless log_skill_use: false is set. Then run sase skill list, sase skill init --dry-run, and finally sase skill init --force when the preview is correct. When use_chezmoi is enabled, sase skill init commits, pushes, and applies the generated files unless passed --no-commit, --no-push, or --no-apply. Do not edit deployed SKILL.md files directly. sase init skills is a compatibility alias for sase skill init.

Provider plugins declare where generated skills should be written. A source can target multiple providers, and a provider can have multiple filesystem targets. Built-in targets are:

Provider Skill target(s)
Claude ~/.claude/skills/<skill>/SKILL.md
Codex ~/.codex/skills/<skill>/SKILL.md
Antigravity (agy) ~/.gemini/antigravity-cli/skills/<skill>/SKILL.md
Qwen ~/.qwen/skills/<skill>/SKILL.md
OpenCode ~/.config/opencode/skills/<skill>/SKILL.md

Bundled Skills

The following skills ship in src/sase/xprompts/skills/ and are deployed by sase skill init. They are packaged with sase, included in sase xprompt list, and available to prompt completion clients even when a checkout does not have local skill files. Coding agents invoke them as /sase_<name>. Runtime config overlays can add more skill sources, so sase skill list may show entries that are not bundled here:

Skill Purpose
sase_agents_status Report on currently-running SASE agents (list, kill, show)
sase_artifact Create explicit SASE artifacts from files produced during an agent run
sase_beads Reference for sase bead commands (create, update, list, ready, show, dep)
sase_chats Inspect prior sase agent chat transcripts via sase chat list and sase chat show
sase_changespecs Inspect and reason about ChangeSpecs via sase changespec search ..., exact-name lookup, and safe edit rules
sase_git_commit Commit changes for git-based VCS via sase commit (the only sanctioned commit path on git repos)
sase_hg_commit Gemini-only commit skill for the hg/fig provider path
sase_memory_read Guide audited long-term memory reads through sase memory read
sase_notify Inspect SASE notification inbox entries via sase notify list and sase notify show
sase_plan Submit a plan file for approval (used in lieu of disabled EnterPlanMode)
sase_questions Ask the user structured questions (used in lieu of disabled AskUserQuestion)
sase_var Attach named output variables to the current SASE agent run

Built-in XPrompts

Core xprompts ship in src/sase/default_config.yml, src/sase/default_xprompts/*.md, and src/sase/xprompts/. They are always available without needing a project- or user-level definition. They're at the built-in end of the discovery order, so any project, user, or config xprompt with the same name overrides the packaged defaults. Common entries include:

Reference Body summary
#cd Switch the agent into a resolved SASE workspace directory
#git Check out a git ref in an isolated workspace and show resulting changes
#commit Create a normal commit from completed agent changes
#propose Create a proposal from completed agent changes
#file Require the agent to write its response to a named markdown artifact
#fork Resume context from a prior agent conversation by name
#fork_by_chat Resume context from a specific chat transcript path
#mentor Run a structured mentor review against a CL
#split_file Ask an agent to split one large Python file into import-safe smaller files
#summarize Summarize a file in a short phrase for a specified use
#json Require the agent response to satisfy a JSON schema
#!sync Sync the current workspace and launch conflict-resolution help if needed
#plan Asks the agent to think the work through and use its /sase_plan skill before any file changes
#epic Marks the request as a multi-phase epic and chains #plan
#legend Marks the request as a larger legend-level planning effort that should later split into epics
#review Asks the agent to fix bugs and apply only clear-win improvements
#prompt/approve Boilerplate "I've edited the previous reply with my decisions; implement this" preamble + #plan
#prompt/review Wraps a prompt input and asks for a gap/ambiguity review before implementation
#research Asks the agent to write a new sdd/research/YYYYMM/ markdown file
#research/image Asks the agent to generate an infographic for an existing research markdown file
#research/more Asks the agent to improve an existing research markdown file by filling missed gaps
#research/prompt Wraps a prompt input and asks for prior art, alternatives, and a recommended solution
#research_swarm Fans out two independent research agents, consolidates their outputs, then generates an infographic
#old_research_swarm Legacy initial, follow-up, and image research workflow (all tagged %g:research)
#x:name,cmd Saves a freeform sase_xcmd command to the prompt (@$(sase_xcmd <name> <cmd>))
#bd/new_epic Multi-phase epic kickoff used by sase bead work (resolved via XPromptTag)
#bd/new_legend Legend kickoff that records epic_count, commits metadata, then runs sase bead work
#bd/work_phase_bead Per-phase agent prompt used by sase bead work
#bd/land_epic Final land-agent prompt used by sase bead work
#bd/next "What should I work on next?" helper that consults the bead tracker
#bd/review/plan Plan-review helper for an epic plan
#bd/review/prompt Prompt-review helper for an epic plan

When #fork / #fork_by_chat injects a # Previous Conversation block, the prior user prompts in that block are sanitized first: sase directives (%name, %wait, %group, ...), #/#! xprompt and workspace references, and any unrendered Jinja2 markers ({{ }}, {% %}, {# #}) are stripped so the forked agent sees clean natural-language text. Fenced code blocks and real markdown headings are preserved, and assistant responses are left untouched. Raw transcripts on disk are unchanged — the cleanup happens only when building resume history (so sase chat show still shows the original prompts).

#bd/new_epic accepts optional ChangeSpec metadata for the plan bead it creates:

#git:sase #bd/new_epic(plan_file_path=sdd/epics/202604/example.md, changespec=sase_feature, bug_id=12345)

When bug_id is supplied, changespec must also be supplied; the generated plan bead is created with the corresponding sase bead create -c/--changespec and -b/--bug-id metadata.

#bd/new_epic also accepts an optional legend_bead_id. When supplied, or when the epic plan file frontmatter contains legend_bead_id, the epic is linked under that legend with --type plan(<plan_file>,<legend_bead_id>) --tier epic. It creates the epic plan bead first, then creates phase beads sequentially in the order they appear in the plan file. Phase bead creation is intentionally not parallelized because child bead suffixes are allocated by creation order. #bd/new_legend creates a --tier legend --epic-count <count> plan bead for sdd/legends/{YYYYMM}/..., writes legend_bead_id, tier: legend, and epic_count frontmatter to the legend plan, commits that metadata, then runs sase bead work <legend_bead_id> --yes.

To see the exact body of any built-in inline xprompt, run sase xprompt expand --trace '#<name>' or browse the catalog with sase xprompt catalog. Use sase xprompt explain <name> for workflows; the explain command takes the workflow name without a # or #! marker.

Bundled Standalone Workflows

The repo-level xprompts/ directory also ships standalone YAML workflows that are normally invoked with #!:

Reference Inputs Purpose
#!sase/fix_just none Repair or validate just workflow issues
#!sase/pylimit_split limits Assist with Python file-size splitting
#!sase/refresh_docs project, gh_ref, threshold Scheduled documentation refresh
#!sase/audit_recent_bugs project, gh_ref, threshold Scheduled audit of recently filed bugs
#!sase/audit_recent_improvements project, gh_ref, threshold Scheduled audit of recently filed improvements

The #!sase/fix_just workflow first bootstraps the workspace with just install, then records the results of just fmt-check, just lint, and just test. Those three checks run before any repair step, so lint/test failures are based on the original checkout state for that workflow run. If formatting failed, the workflow runs just fmt, stages the resulting tree with git add -A, creates a commit when the staged diff is non-empty, then rebases on upstream and pushes. If lint or test failed, it launches separate draft-PR repair agents through #gh:sase #pr(...) for fix_just_linters and fix_just_tests; workflow expansion applies the normal PR context before the agents run.

The scheduled documentation refresh workflow lives in this repo as xprompts/refresh_docs.yml and is invoked as #!sase/refresh_docs. It accepts project, gh_ref, and threshold, defaulting to the main sase repo behavior (project=sase, gh_ref=sase, threshold=50). For scheduled checks in linked repos, pass repo-specific values such as #!sase/refresh_docs(project=sase-core, gh_ref=sase-org/sase-core, threshold=25). The project input selects the marker path under ~/.sase/projects/<project>/refresh_docs_marker; gh_ref is forwarded to the nested docs agent as #gh:<gh_ref>. Scheduled lumberjack agents in sase_athena.yml reach these workflows by embedding #gh:sase-org/sase in their prompts so the sase project workspace is selected before resolution.

Config-Based XPrompts

XPrompts can be defined inline in sase.yml under the xprompts: key.

Simple Format

xprompts:
  propose: "Please propose your changes before applying them."

Structured Format

xprompts:
  greet:
    description: Greet a user a configurable number of times.
    input:
      name:
        type: word
        description: Name to greet.
      count:
        type: int
        default: 1
        description: Number of greetings to render.
    content: "Hello {{ name }}, count is {{ count }}"

Config-based xprompts have priority 6 (below file-backed project and user xprompts; above plugin and built-in definitions).

Standalone workflows must be defined as YAML files in an xprompts/ directory (repo-level, project plugin, or built-in package). Top-level workflows: blocks in sase.yml or sase_*.yml overlays are no longer supported and will be ignored by the runtime; move any such definitions into xprompts/<name>.yml files.

Local Configuration Files

You can define project-specific xprompts in a sase.yml file in the current working directory. This is a full sase config file that can override any configuration, including xprompts. It is the highest-priority config source in the deep-merge system, overriding global sase.yml, overlay files, plugin configs, and built-in defaults. Individual .md files in xprompts directories still take precedence over config-defined xprompts.

xprompts:
  # Simple format — value is the template body
  propose: "Please propose your changes before applying them."

  # Structured format — with typed inputs and/or output
  greet:
    description: Greet a user a configurable number of times.
    input:
      name:
        type: word
        description: Name to greet.
      count:
        type: int
        default: 1
        description: Number of greetings to render.
    content: "Hello {{ name }}, count is {{ count }}"

Directives

Directives are in-prompt tags with a % prefix that modify agent runner behavior. They are extracted and stripped from the prompt before further processing.

Supported Directives

Directive Alias Description
%model %m Override the LLM model for this prompt
%name %n Assign, auto-generate, or force-reuse an agent name
%wait %w Wait for another agent or workflow to succeed
%time %t Defer launch by a duration or until an absolute wall-clock time
%hide %h Hide the agent from the default Agents tab display
%approve %a Run the agent fully autonomously (skip approval)
%epic Enable plan mode and auto-approve the plan as an epic
%edit %e Return editor text to the prompt bar for review
%repeat %r Run the prompt multiple times (e.g., %repeat:3)
%group %g Assign the agent's user-managed tag (e.g., %group:review)
%alt %{} Split prompt into variants with different text (brace shorthand)

Syntax

Directives use the same argument syntax as xprompt references:

%model:claude-sonnet         # Colon syntax
%model(claude-sonnet)        # Parenthesis syntax (single value only)
%model:`claude-sonnet-4`     # Backtick syntax (for values with special chars)
%model:codex/o3              # Provider/model syntax — switches both provider and model
%model:agy/flash35h          # Provider/model syntax for Antigravity (agy)
%model:opencode/anthropic/claude-sonnet-4-5 # Nested provider/model syntax
%name:reviewer               # Short-form
%n:reviewer                  # Same, using alias
%name                        # Bare — auto-generates a unique name
%name:!reviewer              # Force reuse by wiping the previous owner
%wait:agent1                 # Wait for agent1
%w:agent2                    # Wait for agent2 (alias)
%wait                        # Bare — waits for the most recently named agent
%wait:agent1,agent2          # Multi-value: equivalent to two separate %wait: lines
%wait(agent1, agent2)        # Same, paren form
%time:5m                     # Wait for 5 minutes before starting
%t:1h30m                     # Wait for 1 hour 30 minutes (alias)
%time:90s                    # Wait for 90 seconds
%time:1430                   # Wait until 14:30 today (wraps to tomorrow if past)
%time:260415/0900            # Wait until 2026-04-15 at 09:00
%wait:agent1 %time:5m        # Wait for agent1, then a 5-minute floor
%repeat:3                    # Run the prompt 3 times
%r:5                         # Same, using alias
%{#review | #test}           # Brace shorthand: branches split on top-level `|`
%alt(#review,#test)          # Long form: same two variants, comma-separated
%(#review,#test)             # Legacy shorthand, still accepted (prefer `%{...}`)
%{sec=#review | perf=#test}  # Named branches become child name suffixes
%{extra instructions}        # Single branch: split into with/without variants
%approve                     # Run fully autonomously
%a                           # Same, using alias
%edit                        # Return editor text to prompt bar
%e                           # Same, using alias
%epic                        # Enable plan mode and auto-approve the plan as an epic
%group:review                # Assign the tag "review" to this agent
%g:review                    # Same, using alias

The %model directive also supports automatic provider resolution: known model names (e.g., opus, o3, qwen3.6-plus) are automatically mapped to their provider. See Per-Prompt Provider Switching for the full model-to-provider mapping.

The %name and %wait directives can be used without arguments. Bare %name auto-generates a permanent unique name for the agent. Bare %wait resolves to the most recently named agent (raises an error if no previous agent exists).

Agent-name templates contain exactly one @ marker. The marker is not a wildcard; SASE replaces it with the next token from the shared auto-name sequence (0, 1, ..., 9, a, ..., z, 00, ...). For example, with no reserved names, %name:@.cld renders as 0.cld, %name:build-@ renders as build-0, and %name:research.@.final renders as research.0.final. The older terminal -@ form still works, but new allocations now start at token 0 and use the alphanumeric sequence instead of positive integers. Later %wait, #fork, and #resume references can use the same template text; in one multi-agent launch, SASE rewrites those references to the concrete name already planned for that template before spawning dependent agents.

Agent names are permanent IDs. A name that belongs to any existing agent state cannot be reused by a normal %name:<name> launch; SASE cancels the launch before spawning an agent, records the prompt as cancelled, and suggests the lowest free numeric suffix such as <name>1. To deliberately reuse a name, use %name:!<name> from the TUI; the ! form is the explicit confirmation to wipe the previous owner and its persisted system state before launching the new agent with that name. Non-TUI launch surfaces reject %name:!<name> unless they provide an explicit confirmation path.

Named %wait dependencies unblock only after the newest matching agent run has a done.json outcome of "completed". For a multi-agent workflow name, the workflow root and every child agent for that root must complete successfully. Failed, killed, crashed, still-running, malformed, or missing done.json artifacts do not satisfy the wait; the dependent agent stays parked until a later successful run of the same dependency name appears.

When a launch has exactly one explicit %wait:<name> dependency and no explicit %name, SASE can allocate a derived name before spawning the waiting agent: <name>.w1, <name>.w2, and so on, using the first free slot. Multi-value waits, bare %wait, and prompts whose name depends on unresolved xprompt expansion do not get a parent-side derived name. Repeat launches reuse this rule, then chain later repeat slots with %wait:<previous-slot-name>.

If a prompt includes both #fork/#resume and %wait, the fork-derived .f<N> name takes precedence over the wait-derived .w<N> name. The wait still controls launch ordering, but the planned agent name follows the resume/fork lineage.

The %time directive (alias %t) defers launch by a duration or until an absolute wall-clock time:

  • Durations in XhYmZs format (e.g., %time:5m, %time:1h30m, %time:90s). When multiple %time durations are specified, the maximum is used.
  • HHMM — wait until that time today (e.g., %time:1430 for 14:30). If the time has already passed, it wraps to tomorrow.
  • yymmdd/HHMM — wait until a specific date and time (e.g., %time:260415/0900 for 2026-04-15 at 09:00). Raises an error if the target is in the past.

%time and %wait combine freely: dependencies wait first, then the time floor applies.

Absolute time waits cannot be combined with duration waits or with each other.

Bare %time is invalid — %time requires a duration or absolute time argument. Time-shaped values passed to %wait (e.g. %wait:5m) raise an error with a migration hint pointing to %time:5m.

Multi-value directives (%wait, %time, %model, %alt) accept comma-separated arguments to collapse what would otherwise be several lines: %wait:agent_a,agent_b is equivalent to two separate %wait: directives. Backtick-quoted values (e.g. %wait:`a,b`) are treated as a single literal and not split on commas.

The %approve, %edit, and %epic directives are boolean flags — they take no arguments and are simply present or absent.

Example

%model:`claude-sonnet-4-20250514`
%name:code-reviewer
%wait:planner
Review the code changes and provide feedback.

The directives are stripped from the prompt text. The agent will use the specified model, be named "code-reviewer", and will wait for the "planner" agent to complete successfully before running.

Hide Directive

The %hide directive marks an agent as hidden. Hidden agents are not shown in the Agents tab by default — press . to toggle their visibility. This is useful for background agents spawned by axe or workflows that don't need active monitoring:

%hide
%name:background-checker
Run periodic health checks.

Approve Directive

The %approve directive marks an agent as fully autonomous. The agent runs without requiring human approval for plan steps or other checkpoints that would normally pause for user input:

%approve
%name:auto-fixer
Fix the lint errors in the codebase.

Edit Directive

The %edit directive causes the editor text to be loaded into the ACE prompt input bar instead of being submitted directly. This lets you compose a prompt in $EDITOR (via Ctrl+G) and then review or tweak it in the prompt bar before launching an agent:

%edit
Refactor the parser module to use dataclasses.

When the editor closes, the %edit directive is stripped and the remaining text appears in the prompt input bar for further editing. The agent is not launched until you press Enter in the prompt bar.

The returned text is loaded with editor-file semantics: if it contains real multi-agent --- segment separators (outside fenced blocks and outside leading YAML frontmatter), it is returned to the ACE prompt stack as one editable prompt pane per agent segment, and any leading xprompt frontmatter is lifted into the prompt properties panel above the top pane. A buffer with no separators still returns to the bar for review; if it has leading frontmatter, that frontmatter is lifted into the properties panel rather than left as literal pane text.

Plan Approval and Coder Follow-up

SASE's planning workflow is driven by the /sase_plan skill together with the sase plan approval pipeline. An agent drafts a plan and submits it with /sase_plan (or sase plan propose); the plan then pauses for user approval before any execution. In the TUI, the agent shows a PLAN status after submitting the plan for review, then PLAN APPROVED once the user approves it. The %epic directive opts a planning agent into this same pipeline with automatic epic approval (see Epic Directive).

Once the plan is approved, sase launches a follow-up coder agent using the same handoff body as the #coder built-in xprompt (see sase/xprompts/coder.md). #coder takes the approved plan file as its plan_file input, injects it with @, and instructs the agent to implement the plan. By default the coder does not inherit the planner's chat transcript — the plan file is the hand-off artifact. Set SASE_CODER_INHERIT_PLANNER_CHAT=1 to restore the old behavior, in which case a #fork:<planner_name> reference is prepended to the coder prompt so it resumes the planner's session. The coder prompt also carries a %model: directive. A model chosen at approval time wins. When no model is chosen, the follow-up default is resolved at handoff time from the planner agent's concrete provider/model: an active worker override, a matching llm_provider.worker_models entry for the planner's primary lane, then the planner's own provider/model as the fallback. The generated prefix is a concrete %model:<provider>/<model> so the planner's primary context is preserved even if the global worker lane changes before launch (when the planner is missing provider/model metadata the follow-up falls back to a bare %model:worker). Ordinary %model:worker directives written elsewhere still resolve from the current effective worker lane.

Outside the TUI, sase plan shows the same pending PlanApproval notifications plus recent approved and inferred rejected plans. Use sase plan approve <id-prefix> --kind approve|commit|epic|legend|tale to approve from a shell. The approve kind runs the coder without committing an SDD plan; tale commits an SDD tale and runs the coder; epic and legend commit the matching SDD tier and launch the bead follow-up; commit records the approved plan in SDD without launching a coder. -m/--model picks the follow-up agent's model, while -p/--prompt adds extra coder instructions for the approve and tale paths.

Epic Directive

The %epic directive enables plan mode and marks the submitted plan for epic approval. When the agent later submits a plan with /sase_plan or sase plan propose, sase follows the same epic path as the TUI Epic action: it writes the SDD epic files, commits them as needed, initializes beads, and launches the epic follow-up agent. Unlike %approve, %epic is plan-specific and does not automatically answer unrelated questions.

%epic
%name:billing-epic
Plan the billing dashboard epic.

Repeat Directive

The %repeat directive runs the same prompt multiple times. The argument is a positive integer specifying the repeat count:

%repeat:3
%name:linter
Run lint checks on the codebase.

This launches 3 independent agents — each spawned with its own process, workspace, and agent_meta.json, appearing as its own top-level entry in the Agents tab. Fan-out happens at launch time: the directive is consumed when the agents are spawned, so there is no outer loop or TUI affordance ticking through iterations. The slot numbers are appended to the %name base (linter.1, linter.2, linter.3); when %name is omitted the auto-assigned base is used (e.g. a.1, a.2, a.3).

Iterations run sequentially: all N agents are spawned up front and register immediately in the TUI, but iteration k+1 is automatically wait-chained behind iteration k via an injected %wait:<prev_name> directive. This turns %repeat into a serial iteration primitive — each iteration can observe its predecessor's work — without blocking the launcher on any single agent.

Each iteration exposes two iteration-scoped named arguments in the agent's workflow:

Variable Meaning Example with %repeat:5
n Current iteration (1-based) 1, 2, 3, 4, 5
N Total iterations (the %repeat argument) 5

These are threaded through via the SASE_REPEAT_ITERATION and SASE_REPEAT_TOTAL environment variables — the agent runner reads them, converts to ints, and passes them as named args into the workflow so they appear as Jinja2 variables in the prompt body:

%repeat:5
Run test suite batch {{ n }} of {{ N }}.

Stopping a repeat chain early with STOP

A repeat iteration can stop every later slot by setting the reserved STOP output variable before it completes:

%repeat:5
Process the next batch; if there is no more work, run: sase var set STOP=1

Because the slots are already spawned and wait-chained, "stopping" works on wake: when a later slot's %wait on its repeat predecessor resolves, the slot checks that predecessor's STOP output variable. If it is truthy, the slot propagates STOP, finalizes as a successful completed (skipped) slot — recording repeat_stopped: true and stopped_by in its done.json — and exits without claiming a workspace or running its prompt. Keeping the outcome completed lets the stop cascade down the chain through the ordinary %wait resolution, so each remaining slot winds down one wait-check cycle after the previous one. STOP is conservative: "", 0, false, no, and off (case-insensitive) are not-stop; any other value stops the chain. See Cross-Agent Output Variables for how STOP behaves outside repeat chains.

Alt Directive

The %alt directive splits a single prompt into multiple variant prompts, each launched as a separate agent. Each branch replaces the directive span in the output prompt.

The preferred shorthand is %{A | B | ...}, which uses braces and splits branches on top-level | separators:

%{#review | #test | #docs}
Analyze the codebase.

This produces three agents, each with "Analyze the codebase." but with #review, #test, or #docs substituted in place of the directive. Branches can be arbitrary text — xprompt references, directives, plain instructions, or [[text blocks]]. Because branches split only on a top-level |, a comma is ordinary branch text: %{foo, bar | baz} is two branches (foo, bar and baz), not three. Nested (), [], {}, and backtick-quoted spans are not split, and any | inside them is treated literally.

The long form %alt(...) and the legacy %(...) shorthand remain accepted; both use parentheses with comma-separated branches:

%alt(#review, #test, #docs)
%(#review, #test, #docs)

New prompts, completions, snippets, and docs should prefer %{...}. %(...) stays parse-compatible during the migration; it may be removed in a future release.

Named Branches

Branches can be named with id=value. The value is inserted into the spawned prompt and the id becomes the child agent suffix. For example, %name:review %{sec=[[security]] | perf=[[performance]]} launches review.sec and review.perf. Unnamed branches use numeric suffixes while skipping any numeric ids already provided by named branches, so %{2=[[named]] | [[first]] | [[second]]} launches suffixes 2, 1, and 3.

When the same named branch id appears in more than one alt directive, those directives are correlated: values with the same id render into the same child prompt, and a missing id in one correlated directive renders as empty text. Empty renders also collapse adjacent horizontal whitespace, so they do not leave doubled spaces or spaces before punctuation; only spaces and tabs are collapsed, newlines and indentation are preserved, and non-empty branches are untouched. For example:

%name:repo %{a=Describe | b=Explain} how this repo works %{a=in detail}.

This launches repo.a with "Describe how this repo works in detail." and repo.b with "Explain how this repo works.".

Single Branch (With/Without Split)

A single-branch alt is treated as a with/without split — it produces two prompts: one with the branch text and one with the directive removed entirely:

%{Also check for security issues.}
Review this module.

This launches two agents: one with "Also check for security issues. Review this module." and one with just "Review this module."

Cartesian Product

Multiple alt directives can appear in the same prompt. Branch lists with no repeated named ids form a Cartesian product: one agent is launched per combination. Brace and paren forms mix freely:

%{Focus on security | Focus on perf} %{%m:opus | %m:sonnet}
Review this code.

This produces 2 × 2 = 4 agents (every focus area paired with every model). Model directives used as branches inside %{...} participate in the Cartesian product naturally: %{#review | %model:opus} fans out a default-model review branch and an opus branch.

Repeated named ids are the exception to the Cartesian rule. Disjoint named ids and unnamed branches remain Cartesian; only the same explicit id repeated across directives is zipped together.

Multi-Model Fan-Out

The %model directive is single-value. To launch multiple agents in parallel — one per model — put one model directive in each %{...} branch:

%{%m:opus | %m:sonnet}
Review this code for edge cases.

This launches two agents with identical prompts, each using a different model. Each agent appears as a separate entry in the Agents tab. Comma/paren multi-argument syntax (%m(opus,sonnet)) and repeated top-level %model directives are no longer supported; use %{%m:opus | %m:sonnet} instead. Colon syntax (%m:opus) and single-model parentheses (%m(opus)) launch a single agent.

When a prompt fans out to multiple models, the spawned agents share a single base name and carry a runtime suffix so they can be told apart at a glance. Given %{%m:opus | %m:gpt-5.5} %n:foo, the two agents are named foo.cld and foo.cdx. The runtime suffix is a short alias declared by the provider plugin (via the llm_provider_short_name hook) — cld, cdx, agy, qwn, opc for the built-in providers — falling back to the full provider name for plugins that don't declare one. If %name is omitted, a single auto-generated base is allocated and shared (e.g. a.cld / a.cdx) rather than each agent picking its own letter independently. Single-model prompts retain their plain %name value unchanged. When two models share a runtime (e.g. %{%m:opus | %m:sonnet} — both claude), the model name disambiguates the suffix: foo.cld-opus and foo.cld-sonnet. Long model names (e.g. Gemini 3.5 Flash (High)) are replaced with a short alias (flash35h) declared by the provider plugin, so a same-runtime agy fan-out reads as foo.agy-flash35h / foo.agy-flash35l rather than echoing the full model string. Model arguments used for naming are first resolved through xprompt shorthand expansion, while the launched prompt keeps the original %model value. For example, %n:ag %{%m:#flash | %m:#pro} can launch agents named ag.agy-flash35h and ag.agy-pro31h.

Multi-Value Directives

The %wait directive supports multiple occurrences — each adds to the wait list:

%wait:agent1
%wait:agent2
%wait:agent3
Do work after all three agents finish.

Agent dependencies and time floors can be mixed freely:

%wait:agent1
%time:5m
Wait for agent1 to finish, then wait at least 5 minutes from launch.

Command Substitution

XPrompt 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 xprompt 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.

Disabled Regions

You can explicitly disable xprompt expansion for a region of text using the %xprompts_enabled directive:

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

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

The closing %xprompts_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:

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

XPrompt Aliases

XPrompt aliases provide raw text-level substitution that runs before any other xprompt processing. They are defined in the xprompt_aliases config field in sase.yml.

These are global shorthand aliases for xprompt names and raw refs. They are separate from ProjectSpec PROJECT_ALIASES, which map alternate project names such as bob to canonical known projects such as bob-cli at the launch boundary. Project aliases are canonicalized before xprompt expansion so launch artifacts and history store the canonical 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:

xprompt_aliases:
  gh_sase: "gh:sase" # #gh_sase → #gh:sase
  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 xprompt 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: xprompt_aliases for the full field specification.

Recursive Expansion

XPrompt bodies can reference other xprompts. 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).

Multi-Agent Prompts

A single prompt can launch multiple agents by using YAML frontmatter and --- segment separators. SASE plans the segments in document order, but agents do not wait for earlier segments unless you add a dependency such as %wait:<name> or bare %wait. The same ----separator convention also applies inside an xprompt body — see Multi-Agent XPrompts (Library-Defined Fan-Out) below.

Frontmatter Panel (ACE TUI)

In the sase ace prompt input, ad hoc prompt frontmatter has a structured Frontmatter Panel above the prompt stack, with the same field set an xprompt .md file supports (name, description, tags, input, xprompts, skill, snippet). Open or focus it with the prompt NORMAL-mode g= keymap; while the panel owns focus, g= runs the panel's deactivate/apply path. The panel also auto-shows when ACE has lifted leading frontmatter into the stack, such as a multi-agent prompt load or an editor-file return from %edit / whole-stack Ctrl+G. A single prompt recalled from history with leading frontmatter but no segment separator stays one verbatim pane instead of auto-opening the panel. Typing --- in the prompt body is passive during live editing: at the very start it stays literal text, and after content it does not split the active pane. Add a top-level property with a (a picker sourced from the same core schema that backs the editor LSP), edit scalar/list fields inline, delete a field with d, and use R for a live-validated raw-YAML escape hatch. The panel owns the canonical YAML it serializes back onto the prompt, so the multi-agent launch path is unchanged.

The structured input and xprompts fields render as foldable sub-trees (h/l): navigate into them with j/k, then A/e/d (or enter) add, edit, and delete individual items through small typed sub-form modals. The input editor offers a name, a core-validated type (with its one-line rule shown inline), an optional default (blank means required), and a description; the xprompts editor offers a _-prefixed name (validated by the same underscore rule the launch path enforces), content, a compact name:type[=default] inputs field, and a description. A #_helper declared here lights up <ctrl+t>/<ctrl+l> completion and argument hints in every prompt pane exactly like a global xprompt — define a helper in the panel and it is instantly usable below.

Frontmatter-Defined Local XPrompts

YAML frontmatter at the start of a prompt can define local xprompts under the xprompts: key. These are defined once in the frontmatter and each segment receives only the local xprompts it actually references (including transitive dependencies). Local xprompt names must start with _ to distinguish them from global xprompts.

---
xprompts:
  _review_rules: "Always check for error handling and edge cases."
---
#_review_rules
Review the authentication module.

Local xprompts support the same structured format as config-based xprompts (typed inputs, Jinja2 content):

---
xprompts:
  _template:
    input: { target: word }
    content: "Review the {{ target }} module."
---
#_template(auth)

Frontmatter-Declared Inputs

Prompt frontmatter can also declare input: arguments using the same typed shorthand as xprompt files (see Typed Inputs). The declared values are substituted into every segment's {{ name }} placeholders before the agents fan out:

---
input:
  service: word
  retries: { type: int, description: how many times to retry }
  dry_run: { type: bool, default: false }
---
Refactor the {{ service }} module ({{ retries }} retries, dry_run={{ dry_run }}).

When a prompt with required (default-less) inputs is submitted in sase ace, an Input Collection Modal opens after the whole-stack submit: each required input gets a typed, validated field (optional inputs stay collapsed behind a reveal toggle, showing their defaults), and the agents launch only once every value is valid. Non-interactive CLI launches (sase run) cannot prompt, so a required input without a default fails fast with a clear message instead of a cryptic template error — give such inputs a default or launch from the TUI.

Segment Separators

After the frontmatter block is consumed, subsequent --- lines on their own act as segment separators. Each segment launches as a separate agent:

---
xprompts:
  _common: "Follow the project coding conventions."
---
%name:step1
#_common
Implement the new feature.
---
%name:step2
%wait:step1
#_common
Write tests for the new feature.

This launches two agents. step2 starts after step1 succeeds because the second segment includes %wait:step1; if that line were omitted, both agents would be eligible to run independently. Both agents share the _common local xprompt.

Cross-Agent Output Variables

Agents can publish small string values for later waited agents or segments with sase var set KEY=VALUE. Give the producer a stable name and make the consumer wait before referencing the producer's variables. Every producer's variables live under a single reserved agents dictionary keyed by agent name:

%name:build-@
Build the report, then run:
sase var set report_path=dist/report.md status=ok
---
%name:review
%wait:build-@
Review {{ agents["build"].report_path }} after the build status is {{ agents["build"].status }}.

The review prompt is rendered after the build-@ dependency completes, so {{ agents["build"].report_path }} and {{ agents["build"].status }} come from the producer's stored agent_meta.json values. A consumer that has already started will not see later writes.

The agents key is a stable Jinja namespace for the producer, not always the producer's concrete runtime name. Agent-name templates use the template base, so a producer that launches as build-0 from %name:build-@ is read as {{ agents["build"].report_path }}, not agents["build-0"]. The key is otherwise the raw agent name with no identifier munging, so dotted, hyphenated, and digit-leading names all work via bracket access: %name:research.@.final{{ agents["research.final"].report_path }}, and %name:0n.cld{{ agents["0n.cld"].report_path }}. Identifier-safe keys also support attribute access such as {{ agents.build.report_path }}. agents is a reserved agent-run Jinja name; a workflow input named agents collides and fails clearly. Output variables are persisted in the producer's agent_meta.json and also appear in ACE's Agents-tab OUTPUT VARIABLES metadata section. They are visible metadata, not secret storage.

STOP is a reserved output-variable name, but only for %repeat / %r chain continuation: setting it stops later repeat slots (see Stopping a repeat chain early with STOP). It has no special meaning for ordinary %wait consumers, --- segments, or %alt fan-outs — those read it like any other variable, e.g. {{ agents["name"].STOP }}.

ACE renders loaded literal --- multi-agent prompts as a prompt stack: each top-level segment becomes an editable pane, while prompt-level frontmatter and fenced-code separators keep the same parsing rules described below. A #name multi-agent xprompt invocation remains a single pane until launch. During live editing, typed --- lines are ordinary prompt text; add panes explicitly from the prompt-stack controls. Stash restore and marked-agent kill-and-edit can also seed multiple panes, but those paths preserve each selected draft or agent prompt as one pane. Use Enter to choose how to submit stacked panes, g<enter> to launch the selected pane directly, or Ctrl+S to submit the panes together in top-to-bottom order. See the ACE prompt-stack guide for the editing keybindings and the default active-pane behavior.

Rules

  • The first --- pair at the start of the document is treated as YAML frontmatter.
  • After frontmatter is consumed, all subsequent --- lines are segment separators.
  • If there is no frontmatter, ALL --- lines are segment separators.
  • A prompt with frontmatter but only one segment is a single-agent prompt with local xprompts (not multi-agent).
  • --- inside fenced code blocks is not treated as a separator.
  • When a multi-agent prompt is saved to prompt history, each individual segment is also saved as a separate entry. This allows segments to appear independently in the prompt history picker for reuse.

Multi-Agent XPrompts (Library-Defined Fan-Out)

An xprompt itself can be a "multi-agent xprompt": its body contains --- separators (outside fenced blocks), and referencing it as the sole content of a user-prompt segment fans the call out into one agent per body segment. The spawned agents share the same input arguments — each segment is rendered with the same (args) substituted in. The catalog, TUI picker, and completion UI display markdown-defined fan-out xprompts with the inline marker (#name). The older #!name form is still recognized for multi-agent xprompts for compatibility, but new prompts should use #name.

# xprompts/three_phase.md
---
input:
  target: word
---
%name:plan
Draft a plan for {{ target }}.
---
%name:code
%wait:plan
Implement {{ target }} following the plan.
---
%name:review
%wait:code
Review the {{ target }} implementation and propose follow-ups.

Invoking it:

sase run '#three_phase(login)'

…dispatches three agents (plan, code, review), each receiving target=login. The %wait directives chain them sequentially; without %wait they would run in parallel.

Detection happens at dispatch time (after standard parse_multi_prompt), in src/sase/agent/multi_agent_xprompt.py, and applies at every dispatch site (sase run, the TUI agent launcher, the query handler).

Multi-agent xprompts can also be embedded inside a larger prompt. In that case, the first rendered body segment is embedded at the reference location and the remaining rendered body segments become follow-up agent prompts:

sase run '#gh:sase Review this first: #three_phase(login)'

When the call site starts with a VCS workspace reference such as #gh:sase, #git:feature, #hg:branch, or a known-project underscore form such as #gh_sase, that workspace reference is inherited by every generated follow-up segment unless the generated segment already declares its own VCS reference. Leading launch directives stay before the inherited workspace reference, so a prompt like %name:abq #gh:sase #three_phase(login) keeps %name:abq attached to the first generated segment and prefixes #gh:sase onto follow-ups.

Rules and Limitations

  • A sole multi-agent reference replaces the whole segment with its generated segments. An embedded multi-agent reference replaces only that reference with the first generated segment, then appends the remaining generated segments as follow-ups.
  • A user-prompt segment can contain multiple multi-agent xprompt references. They expand fully in document order. Text before the first reference attaches to the first generated segment only; text between references and after the last reference is discarded.
  • Ordinary inline xprompt references inside a multi-agent xprompt body remain inline xprompt references; the agent runner expands them later as normal prompt text.
  • --- inside fenced code blocks in the xprompt body is not treated as a separator.
  • Recursive fan-out (a multi-agent xprompt body whose own segments reference more multi-agent xprompts) is bounded by a depth cap and will raise if exceeded.

Relationship to Workflows

XPrompts and workflows share the same argument grammar, but the marker communicates how the reference is allowed to participate in a prompt:

  • #name(args) expands inline-capable xprompts and workflows with a prompt_part step, including markdown-defined multi-agent xprompts that fan out into multiple prompt segments.
  • #!name(args) launches standalone YAML workflows that have no prompt_part step.

Simple markdown xprompts are converted internally to single-step workflows with a prompt_part step, so they remain inline-capable and continue to use #name, even when their body contains top-level --- segment separators.

YAML workflow files can set a top-level description and use the same input-description forms as markdown or config-defined xprompts:

description: Refresh generated docs and report drift.
input:
  docs_dir:
    type: path
    description: Documentation root to refresh.
steps:
  - name: refresh
    bash: just docs

Workflow agent steps can embed xprompt references inline:

steps:
  - name: review
    agent: |
      #mentor(prompt=[[Review error handling]])

See the Workflow Specification for full details on multi-step workflows, control flow, parallel execution, and human-in-the-loop approval.