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.

The resolution path is easiest to read as a pipeline from prompt references to final prompt text or workflow launches:

Diagram showing user prompt references flowing through xprompt resolution stages into inline prompt text, standalone workflows, workflow graphs, or multi-agent fan-out.

Table of Contents

CLI Subcommands

The sase xprompt command provides five subcommands for working with xprompts.

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; standalone workflows and multi-agent xprompts 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 sibling ../sase-core debug or release binary, then cargo run --manifest-path ../sase-core/Cargo.toml -p sase_xprompt_lsp -- when available.

Examples:

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

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.

Discovery Order

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 memory/long/*.md Auto-discovered dynamic-memory xprompts
7 sase.yml xprompts: section Config-based definitions (local + global)
8 Plugin packages (sase_xprompts EPs) Installed plugin xprompts
9 <sase_package>/default_xprompts/*.md Built-in default markdown xprompts
10 <sase_package>/xprompts/*.md Built-in package xprompts shipped with core SASE

Each directory (priorities 1-6 and 8-10) can contain individual .md files. Within priority 7, 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-6 and 8-10), 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
input:
  user_name: word
---

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 init-skills (see below)
keywords No Trigger terms that append this xprompt as dynamic memory to matching prompts

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

Reference Syntax

Reference inline-capable xprompts inside any prompt with the # prefix. Use #! for standalone references: YAML workflows that do not have a prompt_part step, plus markdown-defined multi-agent xprompts whose body contains top-level --- segment separators. The marker must appear at the start of the string, after whitespace, or after one of ([{"'.

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 or multi-agent xprompt reference, no args
#!name(args) Standalone/multi-agent reference with parenthesized arguments
#!name:arg Standalone/multi-agent 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 VCS 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.

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

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 the Google plugin is installed

Prompts that do not contain a workspace reference are normalized to #cd:~, so a bare prompt still runs from the home directory with no VCS workspace management. Use #cd:/abs/path, #cd:relative/path, #cd:../sibling, #cd:~, or #cd(.) to choose a directory explicitly.

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/*/*.gp. 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.

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
  - name: max_retries
    type: int
    default: 3

Shortform Syntax

input:
  diff_path: path
  max_retries: { type: int, default: 3 }

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

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

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

Dynamic Memory (Keywords)

An xprompt with a keywords front-matter field becomes a dynamic memory: whenever an agent prompt contains text that matches one of the keywords, the memory xprompt is appended to the prompt as additional context. Matched memories are listed in a ### DYNAMIC MEMORY section at the bottom of the prompt (one @<path> entry per memory), so the agent can load them on demand.

---
name: memory_long_external_repos
keywords: [chezmoi, plugin]
---

Repo-layout notes for cross-repo work...

Negative Keywords

Any keyword prefixed with ! is a negative keyword. Before positive-keyword matching runs, each negative keyword is matched against the prompt and its hit spans are masked out. The memory is then excluded only when every positive keyword hit falls inside a masked region. A negative keyword that doesn't cover any positive hit is a no-op.

For example, keywords: [foo, "!/foo/"] matches "update foo and /path/to/foo/" (via the standalone foo) but not "update /path/to/foo/" (both foo occurrences land inside the /foo/ masked span).

YAML gotcha: !-prefixed entries MUST be quoted ("!vendor"), otherwise YAML parses the leading ! as a tag directive and raises an error at load time.

Command Substitution Masking

Keyword matching runs after xprompt expansion, which means $(cmd) substitutions have already been replaced with their stdout. To prevent environment-derived text (e.g. file paths emitted by $(branch_changes ...)) from triggering spurious matches, every $(...) span — including nested parentheses — is masked to spaces before keyword matching runs. Escaped \$(...) is preserved as literal text and remains eligible for matching.

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

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. The sase init-skills command reads this field to determine which xprompts should be rendered into per-provider SKILL.md files and deployed to agent skill directories.

---
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", "gemini"] Deploy only to the listed providers

The description field provides a human-readable summary shown in sase xprompt 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.

Workflow: Edit skill sources in src/sase/xprompts/skills/, run sase init-skills --force, then chezmoi apply to deploy the generated files to their live locations. Do not edit deployed SKILL.md files directly.

Bundled Skills

The following skills ship in src/sase/xprompts/skills/ and are deployed by sase init-skills. 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>:

Skill Purpose
sase_agents_status Report on currently-running sase agents (status, kill, show)
sase_beads Reference for sase bead commands (create, update, list, ready, show, dep)
sase_chats Inspect prior sase agent chat transcripts via sase chats list and sase chats 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 Mercurial counterpart of sase_git_commit
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)

Built-in XPrompts

A handful of xprompts ship in src/sase/default_config.yml and src/sase/default_xprompts/*.md and 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 file-backed markdown defaults.

Reference Body summary
#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 Tells the agent to store research in a new sdd/research/ 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 a research prompt into initial research, follow-up research, and image research agents
#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

#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

The scheduled documentation refresh workflow is configured globally as #!refresh_docs in sase_athena.yml, not as a repo-local project-namespaced workflow. 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 sibling repos, pass repo-specific values such as #!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>.

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:
    input: { name: word, count: { type: int, default: 1 } }
    content: "Hello {{ name }}, count is {{ count }}"

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

Config-Based Standalone Workflows

Standalone workflows can be defined in config files under the top-level workflows: key and invoked with #!name. Their schema matches YAML workflow files:

workflows:
  refresh_docs:
    input:
      project: { type: line, default: "sase" }
    steps:
      - name: run
        bash: echo "{{ project }}"

Config workflows have the same precedence band as config xprompts: below file-based and project-specific workflows, above plugin and built-in workflows. Workflows from local ./sase.yml are namespaced when a project is detected; workflows from user config and sase_*.yml overlays remain global.

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:
    input: { name: word, count: { type: int, default: 1 } }
    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 to succeed, or for a duration
%hide %h Hide the agent from the default Agents tab display
%approve %a Run the agent fully autonomously (skip approval)
%plan %p Enable plan mode (plan first, then execute)
%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)
%tag %t Assign the agent's user-managed tag (e.g., %tag:review)
%alt %( Split prompt into variants with different text

Syntax

Directives use the same argument syntax as xprompt references:

%model:claude-sonnet         # Colon syntax
%model(claude-sonnet)        # Parenthesis syntax
%model:`claude-sonnet-4`     # Backtick syntax (for values with special chars)
%model:codex/o3              # Provider/model syntax — switches both provider and model
%model:gemini/gemini-2.5-pro # Provider/model syntax for Gemini
%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 after confirmation and wipe
%wait:agent1                 # Wait for agent1
%w:agent2                    # Wait for agent2 (alias)
%wait                        # Bare — waits for the most recently named agent
%wait:5m                     # Wait for 5 minutes before starting
%wait:1h30m                  # Wait for 1 hour 30 minutes
%wait:90s                    # Wait for 90 seconds
%wait:1430                   # Wait until 14:30 today (wraps to tomorrow if past)
%wait:260415/0900            # Wait until 2026-04-15 at 09:00
%wait:agent1,agent2,5m       # Multi-value: equivalent to three separate %wait: lines
%wait(agent1, agent2, 5m)    # Same, paren form
%repeat:3                    # Run the prompt 3 times
%r:5                         # Same, using alias
%alt(#review,#test)          # Split into two prompts: one with #review, one with #test
%(#review,#test)             # Same, using shorthand syntax
%alt(sec=#review,perf=#test) # Named variants become child name suffixes
%alt(extra instructions)     # Single arg: split into with/without variants
%(extra instructions)        # Same, using shorthand
%approve                     # Run fully autonomously
%a                           # Same, using alias
%edit                        # Return editor text to prompt bar
%e                           # Same, using alias
%plan                        # Enable plan mode
%p                           # Same, using alias
%epic                        # Enable plan mode and auto-approve the plan as an epic
%tag:review                  # Assign the tag "review" to this agent
%t:review                    # Same, using alias

The %model directive also supports automatic provider resolution: known model names (e.g., opus, o3, gemini-2.5-pro) 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 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. SASE asks for confirmation, wipes the previous owner and its persisted system state, then launches 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.

The %wait directive also accepts duration arguments in XhYmZs format (e.g., %wait:5m, %wait:1h30m, %wait:90s). Duration waits delay the agent's start by the specified amount. When a prompt contains both agent-name waits and duration waits, the agent waits for all named agents to complete successfully and then waits for the remaining duration (the maximum duration is used if multiple are specified).

The %wait directive additionally accepts absolute time arguments:

  • HHMM — wait until that time today (e.g., %wait: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., %wait:260415/0900 for 2026-04-15 at 09:00). Raises an error if the target is in the past.

Absolute time waits cannot be combined with duration waits or with each other. They can, however, be combined with agent-name waits.

Multi-value directives (%wait, %model, %alt) accept comma-separated arguments to collapse what would otherwise be several lines: %wait:agent_a,agent_b,5m is equivalent to three 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, %plan, 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.

Plan Directive

The %plan directive enables plan mode for the agent. The agent first creates a plan and pauses for user approval before proceeding with execution. In the TUI, the agent shows a PLANNING status while creating the plan, then PLAN APPROVED once the user approves it:

%plan
%name:refactorer
Refactor the authentication module to use the new middleware.

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 #resume:<planner_name> reference is prepended to the coder prompt so it resumes the planner's session.

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

Alt Directive

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

%alt(#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. Arguments can be arbitrary text — xprompt references, directives, plain instructions, or [[text blocks]].

Arguments can also 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 %alt(sec=[[security]],perf=[[performance]]) launches review.sec and review.perf. Unnamed alternatives use numeric suffixes while skipping any numeric ids already provided by named arguments, so %(2=[[named]], [[first]], [[second]]) launches suffixes 2, 1, and 3.

%(...) is syntactic sugar for %alt(...):

%(#review,#test)
Analyze the codebase.

A single-argument %alt is treated as a with/without split — it produces two prompts: one with the argument 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."

Multiple %alt/%( directives can appear in the same prompt. When they do, a Cartesian product of all argument lists is computed — one agent is launched per combination:

%(Focus on security, Focus on perf) %m(opus, sonnet)
Review this code.

This produces 2 × 2 = 4 agents (every focus area paired with every model). The multi-model directive (%m(opus,sonnet)) is internally rewritten as %alt(%model:opus,%model:sonnet), so it participates in the Cartesian product naturally.

Multi-Model Directive

The %model directive supports launching multiple agents in parallel — one per model — when given comma-separated model names in parentheses:

%m(opus,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. Only the parenthesized syntax triggers multi-model behavior; colon syntax (%m:opus) and single-model parentheses (%m(opus)) always 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,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, gem, 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,sonnet) — both claude), the model name disambiguates the suffix: foo.cld-opus and foo.cld-sonnet. Long model names (e.g. gemini-3-flash-preview) are replaced with a short alias (flash3) declared by the provider plugin, so a same-runtime gemini fan-out reads as foo.gem-flash3 / foo.gem-flash25 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,#pro) can launch agents named ag.gem-flash3 and ag.gem-pro31p.

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 names and durations can be mixed freely:

%wait:agent1
%wait: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.

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 sequentially by using YAML frontmatter and --- segment separators. The same ----separator convention also applies inside an xprompt body — see Multi-Agent XPrompts (Library-Defined Fan-Out) 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)

Segment Separators

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

---
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: step1 runs first, then step2 starts after step1 succeeds (via %wait). Both agents share the _common local xprompt.

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 standalone marker (#!name) because they expand into multiple agent prompts. The legacy #name form is still recognized for multi-agent xprompts, but new prompts should prefer #!name for clarity.

# 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 user-prompt segment can contain at most one multi-agent xprompt reference. Split the prompt manually if you need to combine multiple fan-outs.
  • 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.
  • 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.
  • #!name(args) launches standalone YAML workflows that have no prompt_part step and marks markdown-defined multi-agent xprompts that expand into multiple prompt segments.

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 unless their body contains top-level --- segment separators.

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.