Automation: Macros

Lectic supports a simple but powerful macro system that allows you to define and reuse snippets of text. This is useful for saving frequently used prompts, automating repetitive workflows, and composing complex, multi-step commands.

Macros are defined in your YAML configuration (either in a .lec file’s header or in an included configuration file).

Defining Macros

Macros are defined under the macros key. Each macro must have a name and an expansion. You can optionally provide an env map to set default environment variables for the expansion.

macros:
  - name: summarize
    expansion: >
      Please provide a concise, single-paragraph summary of our
      conversation so far, focusing on the key decisions made and
      conclusions reached.

  - name: build
    env:
      BUILD_DIR: ./dist
    expansion: exec:echo "Building in $BUILD_DIR"

Expansion Sources

The expansion field can be a simple string, or it can load its content from a file or from the output of a command, just like the prompt field. For full semantics of file: and exec:, see External Prompts.

  • File Source: expansion: file:./prompts/summarize.txt
  • Command/Script Source:
    • Single line: expansion: exec:get-prompt-from-db --name summarize (executed directly, not via a shell)

    • Multi‑line script: start with a shebang, e.g.

      expansion: |
        exec:#!/usr/bin/env bash
        echo "Hello, ${TARGET}!"

      Multi‑line scripts are written to a temp file and executed with the interpreter given by the shebang.

Using Macros

To use a macro, you invoke it by writing the macro name as the directive name:

  • :name[] expands the macro.
  • :name[args] expands the macro and also passes args to the expansion as the ARG environment variable.

When Lectic processes the file, it replaces the macro directive with the full text from its expansion field before processing any other directives (like :cmd).

This was a long and productive discussion. Could you wrap it up?

:summarize[]

The Macro Expansion Environment

When a macro expands via exec, the script being executed can be pased information via environment variables.

Passing arguments to expansions via ARG

The text inside the directive brackets is passed to the macro expansion as the ARG environment variable.

This works for both single-line exec: commands and multi-line exec: scripts.

  • :name[hello] sets ARG=hello.
  • If you explicitly set an ARG attribute, it overrides the bracket content: :name[hello]{ARG="override"}.

Passing other environment variables via attributes

You can pass environment variables to a macro’s expansion by adding attributes to the macro directive. These attributes are injected into the environment of exec: expansions when they run.

  • :name[]{FOO="bar"} sets the variable FOO to bar.
  • :name[]{EMPTY} sets the variable EMPTY to be undefined. If you need an empty string value, write :name[]{EMPTY=""}.

Notes: - Single‑line exec: commands are not run through a shell. If you need shell features, invoke a shell explicitly, e.g., exec: bash -c 'echo "Hello, $TARGET"'. - In single‑line commands, variables in the command string are expanded before execution. For multi‑line scripts, variables are available to the script via the environment.

Example

Configuration:

macros:
  - name: greet
    expansion: exec: bash -c 'echo "Hello, $TARGET!"'

Conversation:

:greet[]{TARGET="World"}

When Lectic processes this, the directive will be replaced by the output of the exec command, which is “Hello, World!”.

Other Environment Variables

A few other environment variables are available by default.

Name Description
MESSAGE_INDEX Index (starting from one) of the message containing the macro
MESSAGES_LENGTH Total number of messages in the conversation

These might be useful for conditionally running only if the macro is, e.g. part of the most recent user message.

Advanced Macros: Phases and Recursion

Macros can interact with each other recursively. To support complex workflows, macros can define two separate expansion phases: pre and post.

  • pre: Expanded when the macro is first encountered (pre-order traversal). If pre returns content, the expansion stops there: the macro is replaced by the result of pre and that result is then recursively expanded. The original children of the macro are discarded.
  • post: Expanded after the macro’s children have been processed (post-order traversal). The processed output of the children is passed to post as the ARG variable.

If you define a macro with just expansion, it is treated as a post phase macro.

Handling “No Operation” in Pre

If the pre script runs but produces no output (an empty string), Lectic treats this as a “pass-through”. The macro is NOT replaced; instead, Lectic proceeds to process the macro’s children and then runs the post phase.

This makes it easy to implement cache checks or conditional logic.

Tip

If you explicitly want to delete a node during the pre phase (stopping recursion and producing no output), you cannot return an empty string. Instead, return an empty HTML comment: <!-- -->. This stops recursion and renders as nothing.

Example: Caching

This design allows for powerful compositions, such as a caching macro that wraps expensive operations.

macros:
  - name: cache
    # Check for cache hit. If found, cat the file.
    # If not found, the script produces no output (empty string),
    # so Lectic proceeds to expand the children.
    pre: |
      exec:#!/bin/bash
      HASH=$(echo "$ARG" | md5sum | cut -d' ' -f1)
      if [ -f "/tmp/cache/$HASH" ]; then
        cat "/tmp/cache/$HASH"
      fi
    # If we reached post, it means pre didn't return anything (cache miss).
    # We now have the result of the children in ARG. Save it and output it.
    post: |
      exec:#!/bin/bash
      HASH=$(echo "$ARG" | md5sum | cut -d' ' -f1)
      mkdir -p /tmp/cache
      echo "$ARG" > "/tmp/cache/$HASH"
      echo "$ARG"

Usage:

:cache[:summarize[:cat[file.txt]]]
  1. :cache’s pre runs. If the cache exists for the raw text of the children, it returns the cached summary. Lectic replaces the :cache block with this text and is done.
  2. If pre returns nothing (cache miss), Lectic enters the children.
  3. :cat expands to the file content.
  4. :summarize processes that content.
  5. Finally, :cache’s post runs. ARG contains the summary. It writes ARG to the cache and outputs it.