Learning in public
Learning in public
A TIL (“today I learned”) stream where you drop a short note and an agent gets nudged to polish and publish it.
What it produces
A folder
TIL/where you drop short notes (1-3 paragraphs each).A
file:createdtrigger that splices a> [!ai]directive into each new TIL, asking the connected agent to clean it up and mark itpublished: true.Once notes carry
published: true,lettuce publishincludes them in the static site (the published set is the whole vault filtered by thepublishedflag — there is no per-folder include list).Optional: a directive can also ask the agent to cross-post the TIL somewhere, since the agent runs whatever you connect it to.
Setup
Convention
# TIL/2026-05-28 swift-pm copies subdirs.md
---
title: "Swift Package Manager copies, doesn't process, when you want subdir structure"
date: 2026-05-28
tags: [til, swift]
---
I spent an hour today wondering why my `Bundle.module.url(forResource: "Docs", ...)` was returning nil. Turns out: `.process("Resources")` flattens subdirectories. To preserve the tree, use `.copy("Docs")` as a separate resource entry.
```swift
resources: [
.process("Resources"),
.copy("Docs"),
],
This now correctly bundles Docs/Getting started/01 Install.md etc. with the directory structure intact.
### Trigger: nudge the agent to publish
A trigger rule has exactly two keys — `when` (always `file:created:<glob>`) and `prompt`. When a new file matches the glob, Lettuce splices a single `> [!ai] <prompt>` directive into the note (idempotent). It does not edit frontmatter or add tags itself — the connected agent does that work after it sees the directive via `pending_directives`.
```yaml
# .lettuce/triggers.yml
- when: "file:created:TIL/*.md"
prompt: >
This is a new TIL. Tighten it to 1-3 paragraphs, add the `til` tag and
any topic tags, then set `published: true` in the frontmatter.
To also cross-post, fold that into the same prompt — the agent runs whatever tools you have connected:
- when: "file:created:TIL/*.md"
prompt: >
This is a new TIL. Tighten it, tag it `til`, and set `published: true`.
Then write a tweet-length (< 280 char) version that ends with the note's
published link, post it via the cross-post tool, and record the returned
URL in the note's body.
Publish
lettuce publish builds the static site from every note whose frontmatter has published: true, so once the agent flips that flag the TIL is part of the next build:
lettuce publish ~/Vault --base-url https://notes.example.com
lettuce deploy ~/Vault --repo me/notes --base-url https://notes.example.com
You can run these by hand, or have a schedule splice a maintenance directive (see below) so a connected agent keeps the site fresh. Once a TIL carries published: true, the next publish/deploy puts it online.
Variants
Daily summary on the index
Keep a “Recent TILs” list on your index.md by hand, or let a schedule nudge an agent to keep it current. A schedule rule splices a > [!ai] <prompt> directive into a target note at a fixed time (daily: "HH:MM" or weekly: "<weekday> HH:MM"); the connected agent does the editing.
# .lettuce/schedules.yml
- daily: "23:00"
target: "index.md"
prompt: >
Refresh the "Recent TILs" section with links to the five most recently
updated notes in TIL/.
There is no built-in templating, so the index is filled in by the agent, not by a publish-time macro.
Weekly roundup
# .lettuce/schedules.yml
- weekly: "Fri 17:00"
target: "TIL/Weekly/this-week.md"
prompt: >
Bundle this week's TILs into this note. Tag it [til, roundup] and set
`published: true` so the next publish picks it up.
The schedule fires from the SchedulePoller, which checks once a minute while lettuce serve (or the app) is running. There is no cron syntax, no per-rule agent: block, and no {{ }} templating — the rule only carries a prompt (and an optional action: for a maintenance pass like lint-semantic); everything else is the agent’s job.