Testing automations
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:
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."Start the server and leave it running past that time:
lettuce serve my-vaultAfter the minute ticks over, read the target — the
> [!ai]directive should be there. Thetarget:template understands{{date}}(YYYY-MM-DD),{{year}}, and{{week}}(week-of-year, system calendar, two digits), soDaily/{{date}}.mdresolves 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>":
*matches anything within one path segment;**matches zero or more whole segments. SoInbox/*matchesInbox/a.mdbut notInbox/sub/a.md, whileInbox/**matches both.**/*.mdmatches every markdown file anywhere in the vault.Paths are vault-relative, so don’t include a leading slash or the vault name.
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.