Skip to content

Multi-agent prompts and workflows

This page documents prompt segments, output variables, macro swarms, and their relationship to workflows.

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 a macro body -- see Macro Swarms (Library-Defined Fan-Out) below.

Within one multi-agent launch, bare %wait / %w in any segment after the first means "wait for the previous launched segment." Empty or whitespace-only parentheses (%wait(), %wait( )) count as bare, including when the wait is introduced by macro expansion. If the previous segment fans out, the dependency targets that segment's last launched child; siblings in the current segment all inherit the same previous-segment dependency. Explicit waits such as %wait:agent, bead/time waits, %queue, and waits inside fenced code or disabled macro regions keep their normal meanings. If the predecessor name is not known yet, SASE records the predecessor artifact identity and waits on that agent or session completion instead of resolving bare %wait against the global latest agent.

Frontmatter Panel (sase's TUI)

In the sase tui prompt input, ad hoc prompt frontmatter has a structured Frontmatter Panel above the prompt stack, with the same field set a macro .md file supports (name, description, tags, input, macros, skill, snippet). Open or focus it with the prompt NORMAL-mode g= keymap; in the panel's rows mode, g= runs the deactivate/apply path. q or Esc in rows mode—or from NORMAL mode inside any panel sub-editor—returns focus to the prompt pane you entered from; an invalid raw-YAML buffer remains open so it cannot be discarded accidentally. In rows mode, gj jumps directly to the top prompt pane and gk to the bottom pane. The panel also auto-shows when sase's TUI has lifted leading frontmatter into the stack, such as a multi-agent prompt load or an editor-file return from a @ review marker / 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 (an inline picker sourced from the same core schema that backs the editor LSP), edit scalar/list fields inline, delete a field with d, undo the latest mutation with u, and use R for a live-validated raw-YAML escape hatch. In raw mode, Ctrl+C explicitly discards an unparseable buffer. Unknown frontmatter keys remain visible as raw-only rows and survive structured round trips.

The structured input and macros fields render as foldable sub-trees (h/l): navigate into them with j/k, use o/A to insert a ghost row, e/enter to edit an item in place, d to delete, and J/K to reorder. Cell editing uses Tab/Shift+Tab; Enter commits while remaining in the panel. Input types cycle through the core type catalog and defaults are live-coerced. Local-helper content uses a bounded multiline editor in the panel. A #_helper declared here lights up <ctrl+t>/<ctrl+l> completion and argument hints in every prompt pane exactly like a global macro — define a helper in the panel and it is instantly usable below.

sase's TUI can also author existing definitions without $EDITOR. In the Macro Browser, Enter loads a simple Markdown or config-backed definition as raw body plus structured frontmatter; E keeps the external-editor path, and YAML workflow graphs remain editor-only. A loaded definition is bound to its source: the prompt title shows the source and a dirty dot, gw writes it atomically, and an external-change conflict offers overwrite, reload, or save-as. gd on a #name reference loads that definition after stashing the current draft. gX is a one-screen save-as view with name, location, resolved path, and a live collision/overwrite preview.

Frontmatter-Defined Local Macros

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

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

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

---
macros:
  _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 macro 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 or live raw placeholders is submitted in sase tui, the Fill in this prompt panel opens after the whole-stack submit. Raw-placeholder fields appear first, followed by typed, live-validated required inputs; optional inputs stay collapsed behind a reveal toggle and show their defaults when opened. Enter advances through visible fields and launches from the last one once every required value is valid. Ctrl+L keeps the focused raw placeholder literal. Escape cancels and returns to the draft; from field INSERT mode, the first press returns to NORMAL and the second cancels. path fields reuse Ctrl+T path completion. See Raw Prompt Placeholders for matching, substitution, and the collection toggle.

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:

---
macros:
  _common: "Follow the project coding conventions."
---
%id:step1
#_common
Implement the new feature.
---
%id: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 macro.

Cross-Agent Output Variables

Agents can publish small JSON-shaped values for later waited agents or segments with sase var set. Values may be strings, numbers, booleans, null, lists, or maps nested within the documented reliability limits. 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:

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

Use a heredoc through --value-file - for a multi-line value:

sase var set summary --value-file - <<'EOF'
Tests passed.

The release artifact is ready for review.
EOF

Add -j / --json to any input form to decode a structured value. Structured values remain real containers in Jinja, so consumers can access and iterate them directly:

%id:build-@
Run the build, then publish:
sase var set report --json --value '{"passed":true,"suites":["unit","integration"]}'
---
%id:review
%wait:build-@
Build passed: {{ agents["build"].report.passed }}
{% for suite in agents["build"].report.suites %}
- Review the {{ suite }} results.
{% endfor %}

Rendering a whole container with {{ agents["build"].report }} produces compact JSON instead of a Python representation. Jinja's | tojson filter remains available for explicit formatting. Map keys are sorted for stable storage and display, while list order is preserved. Run sase var get to inspect the current agent's canonical block display or sase var get --format json for compact JSON. Quote a wrapped name such as sase var get '<build>' --format json for another agent's newest snapshot; sase var get build.* remains selector mode.

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 %id: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: %id:research.@.final → {{ agents["research.final"].report_path }}, and %id: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 sase's TUI Agents-tab OUTPUT VARIABLES metadata section and Telegram agent-completion messages. 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 }}.

A planner that submits a plan for review (sase plan propose) exposes the proposed plan path as a synthesized plan_file variable in the same agents namespace, without any sase var set call. A later segment in the same multi-agent prompt reads it under the producer's stable key, and an explicit %wait on the planner row reads it under the canonical <base>--plan key:

%id:planner
Propose a plan for the feature.
---
%id:coder
%wait:planner--plan
Implement the plan at {{ agents["planner--plan"].plan_file }}.

plan_file is only ever namespaced under agents[...]; there is no top-level plan_file variable. An explicit sase var set plan_file=... in the planner wins over the synthesized value, and any other output variables the planner sets are preserved alongside it.

sase artifact create exposes a SASE-managed artifacts list in the same agents namespace, without any sase var set call. A research-swarm researcher registers its report with sase artifact create, and a later %wait consumer renders it as {{ agents["research.final"].artifacts[0].ref }} or loops {% for a in agents["research.final"].artifacts %}{{ a.label }}{% endfor %}. Entries share field names (ref, label, kind, path, source_path) with wait.artifacts. Every waited target with a known epic also synthesizes created_epic (the first epic it launched) and created_epics (every epic it launched) under the same agents[...] key. Values come from the waiter's FOLLOWING entry when the wait is following that target, else from the target's own created_epics record — so a for_epic=false consumer still gets the ID. An explicit sase var set of either name wins over the synthesized value.

sase's TUI 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 macro swarm 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. By default, use Enter to open the submission panel and press Enter again to confirm its primary launch action; use g<enter> or Ctrl+G Enter to launch the selected pane directly, or Ctrl+S to stash the active pane. Inside the Enter submit chooser, a or Ctrl+S submits all panes top-to-bottom. Set ace.prompt_submission.confirm_on_enter: false to make plain Enter submit the active pane immediately, including in a stack. See the sase's TUI 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 macros (not multi-agent).
  • --- inside fenced code blocks is not treated as a separator.
  • When a multi-agent prompt a user submitted 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. Segment recording applies only to user-submitted multi-prompts: machine-originated launches write no history rows at all.

Macro Swarms (Library-Defined Fan-Out)

A macro itself can be a macro swarm: 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 macro swarms with the inline marker (#name). The older #!name form is still recognized for macro swarms for compatibility, but new prompts should use #name.

# sase/macros/three_phase.md
---
input:
  target: word
---
%id:plan
Draft a plan for {{ target }}.
---
%id:code
%wait:plan
Implement {{ target }} following the plan.
---
%id: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.

For swarm-owned generated hoods, use keyed agent-name markers so every segment and prose reference resolves in the parent launch before any agent starts:

%id:research.{@1}.cdx
Audit with Codex. Write your report path for `research.{@1}.cdx`.
---
%id:research.{@1}.cld
Audit with Claude. Write your report path for `research.{@1}.cld`.
---
%id(final, clan=research.{@1})
%wait:research.{@1}.cdx,research.{@1}.cld
Summarize both reports.

During swarm expansion, each unqualified {@1} is rewritten to an invocation-specific qualified key before dispatch, so overlapping launches of the same macro cannot steal each other's clan or hood. Use {@shared!} only when a nested swarm should intentionally share a key with its caller. See Directives for the complete keyed-marker grammar and dispatch-scoping rules.

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

Macro swarms 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 '+sase Review this first: #three_phase(login)'

When the call site starts with a workspace reference such as +sase, #gh:sase, #git:feature, a plugin-provided ref, 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 %id:abq +sase #three_phase(login) keeps %id:abq attached to the first generated segment and prefixes +sase onto follow-ups.

Rules and Limitations

  • A sole macro swarm reference replaces the whole segment with its generated segments. An embedded macro swarm 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 macro swarm 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 macro references inside a macro swarm body remain inline macro references; the agent runner expands them later as normal prompt text.
  • --- inside fenced code blocks in the macro body is not treated as a separator.
  • Recursive fan-out (a macro swarm body whose own segments reference more macro swarms) is bounded by a depth cap and will raise if exceeded.

Relationship to Workflows

Macros 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 macros and workflows with a prompt_part step, including markdown-defined macro swarms that fan out into multiple prompt segments.
  • #!name(args) launches standalone YAML workflows that have no prompt_part step.

Simple markdown macros 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 macros:

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

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