Macros

Macros are named, reusable sequences of queries — the same prompts and slash commands you’d otherwise type by hand — that you define in your configuration or profile and invoke from the prompt with the ! sigil:

!explain_folder path=./src/

When a macro runs, its queries: execute in order, each one treated exactly as if you had typed it yourself. That includes slash commands (/toolset load ..., /session push ...) and calls to other macros: a nested macro call is expanded at its call site and executed in sequence, and when the inner macro finishes (or breaks out early), Enkaidu resumes the calling macro where it left off.

Where macros live

A macro can be defined in either place:

  • Your main config file, under a top-level macros: section (see Configuration).
  • Your profile folder .enkaidu/: either a macros.yaml/macros.yml file, or any YAML file inside the macros/ folder (files in sub-folders count too).

When a macro name exists in both places, the config definition wins, and each profile macro with that name is superseded. /macro ls lists every macro you have, along with its origin (Config or Profile) and description.

Every macro definition is a map with just two properties:

  • description: — a description of the macro (required).
  • queries: — an array of queries, executed in order when the macro is invoked.
compact:
  description: Compact the current session with a summary
  queries:
    - /session push system_prompt_name=compact
    - /prompt use compact
    - /session pop_and_take response_only=yes reset_parent=yes

Parameters

Macros can take positional and named parameters. In a macro’s queries, patterns of the form %{WORD} are replaced with arguments collected at the call site at invocation time — before any query executes.

  • Positional parameters are %{N} where N is a positive integer — but note the macro call itself counts as argument 0, so your first argument is %{1}. Positional arguments must precede named ones at the call site.
  • Named parameters are %{WORD} patterns, filled from word=value pairs at the call site.
macros:
  explain_folder:
    description: Explain the contents of a folder
    queries:
      - |
        Explain the purpose and structure
        of the code in %{path}

This macro is invoked like !explain_folder path=./src/, with %{path} replaced by ./src/.

A macro call is aborted (with a warning naming the missing parameter) if any required positional or named argument is not supplied.

To include text that merely looks like a parameter pattern without it being interpolated, double the percent: %%{not_a_param} stays literal.

Branching inside macros

Macros run their queries strictly in order, so out of the box they can’t react to what happened mid-run. That’s what conditional commands are for: a ?break if... / ?break unless... line terminates the current (innermost) macro, skipping its remaining queries.

Consider an example where the guard is the macro.

add_dependency:
  description: |
    Add a dependency to requirements.txt, skipping the addition if the
    file already lists it.
    - Usage: `!add_dependency httpx`
  queries:
    - ?break if file=requirements.txt contains=%{1}
    - Add `%{1}`, pinned to its latest stable version, to requirements.txt.

Here ?break sits at the macro’s top level: if the dependency is already listed, firing the guard terminates the whole macro and the second query never runs.

All the forms of conditional commands are documented in Conditional Commands.

Blocks New in 0.9.11 Experimental

Query entries can also be blocks — nested groups of queries keyed on <enter> — which ?break exits early while execution continues after the block.

The <enter> block-entry syntax, and whether block-entry commands will eventually support parameters, may still change.

One motivation for blocks and breaks is the idempotent macro: one you can safely run again tomorrow without redoing the setup.

changelog_entry:
  description: |
    Add a bullet under "Unreleased" in CHANGELOG.md, creating the file
    first only when it doesn't exist yet.
    - Usage: `!changelog_entry "Added block-aware macros"`
  queries:
    - <enter>:
        - ?break if file_exists=CHANGELOG.md
        - |
          Create a CHANGELOG.md in the project root with a "# Changelog"
          heading and an empty "## [Unreleased]" section.
    - |
      Add this entry as a bullet under the "Unreleased" section of
      CHANGELOG.md: %{1}

Read it the way you’d run it. On the first run there’s no CHANGELOG.md, the guard doesn’t fire, so the block scaffolds the file and the block ends naturally; execution continues and the entry gets appended. On every run after, the guard fires immediately, the block’s creation query is skipped, and execution resumes at the append. Without the break, the “create the file” query would clobber an existing changelog on every run.

At the prompt

  • Type ! and press Tab to see all available macros (auto-completion works on macro names as you keep typing).
  • While you type, Enkaidu highlights macro calls so you can see that a query will be treated as a macro invocation.