Testing automations

~4 min read

Testing automations

There is no dedicated dry-run command for triggers or schedules — they’re small enough to test by exercising the real thing. Remember what they actually do: a trigger splices a > [!ai] <prompt> directive into a newly created note, and a schedule either splices that directive into a target note or runs a maintenance action:. The directive then shows up in the pending_directives MCP tool, and whatever agent is connected does the real work. So “testing” mostly means confirming the directive lands where you expect.

Test a trigger

A trigger fires on file:created:<glob>. To test one, just create a note that matches the glob and check that the directive was spliced in. Given this rule:

# .lettuce/triggers.yml
- when: "file:created:Inbox/**"
  prompt: "Triage this note and suggest a home for it."

create a matching note (in Lettuce, or by dropping a file at Inbox/scratch.md), then open it again. You should see the prompt appended as a callout:

# scratch

> [!ai] Triage this note and suggest a home for it.

Splicing is idempotent: if the same directive (verbatim) is already in the note, it isn’t added a second time. So creating the note again, or saving over it, won’t pile up duplicate directives.

If nothing was appended, the glob didn’t match — see Glob debugging below.

Test a schedule

Schedules fire on a clock (daily: "HH:MM" or weekly: "<Day> HH:MM") while lettuce serve is running (the poller ticks every 60 seconds), and they remember the last fire in .lettuce/schedule-state.json so they never double-fire. There’s no “run now” command, so to test one quickly:

  1. Set the time to a minute or two from now and point target: at a throwaway note:

    # .lettuce/schedules.yml
    - daily: "14:05"
      target: "scratch/test.md"
      prompt: "This is a scheduled directive."
    
  2. Start the server and leave it running past that time:

    lettuce serve my-vault
    
  3. After the minute ticks over, read the target — the > [!ai] directive should be there. The target: template understands {{date}} (YYYY-MM-DD), {{year}}, and {{week}} (week-of-year, system calendar, two digits), so Daily/{{date}}.md resolves to today’s note.

A schedule with an action: instead of relying on a prompt runs a maintenance pass directly. The two supported actions are lint-semantic and maintain. You can run the same semantic health check by hand to see what it would file, without waiting for the schedule. Add --file to actually splice the findings in (this is what action: lint-semantic does); leave it off to just print:

lettuce lint my-vault --semantic --file

That closes the lint loop — it files each new finding as a > [!ai] directive — exactly as a scheduled action: lint-semantic would, deduped against .lettuce/lint-state.json. (--semantic needs an AI provider key.)

Check what’s pending

Both triggers and schedules ultimately produce > [!ai] directives. To see how many are waiting for an agent across the vault, check the server’s health summary, which includes a pendingDirectives count, or call the pending_directives MCP tool from a connected client to list them.

Glob debugging

When a trigger isn’t matching, the cause is almost always the glob in when: "file:created:<glob>":

YAML that doesn’t parse

triggers.yml and schedules.yml are plain YAML lists. If the file fails to parse, Lettuce silently loads no rules rather than erroring — so a file that “does nothing” is often a syntax slip. A trigger entry must have exactly when: and prompt:; a schedule entry must have daily: or weekly:, a prompt, and a target: (plus an optional action:). Lint the YAML with any validator, and double-check you didn’t leave in keys from another tool — there are no if:/do:/cron:/every:/agent: fields here.