AI Agents & Automation

Church starts at 10. The worksheet lands at 9:20.

Brett Ridenour Brett Ridenour · Published November 2026

I wanted a fill-in-the-blank sheet for Sunday sermons. The kind you had in high school youth group, where the preacher says a word and you fill in the underline, and a week later you can still remember what the message was because your hand wrote it.

The problem: the service I go to is at 10am. The one that gets streamed is at 8am. Nobody is going to hand me a worksheet for a sermon I haven’t heard yet.

So I built the thing that does that. It’s a cron. It fires at 7:44, waits for the 8am stream to go live, records the audio, transcribes it, writes a worksheet with Opus, has two subagents review the draft, renders a PDF, and DMs it to me on Slack before the 10am service starts. First production run is this Sunday.

Here’s how it works.

The timer is the hard constraint

The whole thing is deadline-driven. A worksheet that arrives at 10:30 is useless — the service has already started. A worksheet that misses the last five minutes of the sermon is fine; you cover it with the blank note lines on the back.

That one rule pushes the design everywhere.

sermon-worksheet.timer

Persistent=false matters. If my desktop is asleep at 7:44 and comes back at 10:30, I don’t want the timer to fire and spend six minutes of Gemini and Opus budget producing a PDF for a service that’s already happening. Miss the window, skip the week.

Recording live, in chunks, so transcription starts before the sermon ends

The naive version is: record the whole service, transcribe it, write the worksheet. That doesn’t fit in the window. The sermon ends around 9:05. A one-pass transcript of a 60-minute service takes real minutes. Then Opus needs 4–8 minutes. Then rendering. Then Slack upload. Everything has to overlap.

So yt-dlp pipes the live HLS audio stream straight into ffmpeg, which cuts 2-minute mp3 segments. The moment each segment closes, a worker thread hands it to Gemini for transcription. By the time the sermon ends, most of the transcript is already on disk.

Live audio piped from yt-dlp into ffmpeg segments

Gemini also plays a second role: after every new segment, it reads the transcript so far and answers one question — has the sermon ended yet? Not “has someone stopped talking,” but “closing prayer is done, the band is back, people are being dismissed.” The moment it says yes twice in a row, recording stops and the writing phase kicks off. If the detector gets confused, there’s a 9:15 hard deadline that cuts everything off anyway.

Two agents with opposite jobs

Here’s the part I’m proud of. Opus writes a draft worksheet as structured JSON — not a document, so it’s checkable. Then two subagents review it in parallel, and the whole point is that they want different things.

The two reviewer subagents

pew-tester is told it’s a 30-year-old man who goes to church every week but has never taken notes before, sitting in the pew with a printed worksheet and a pen. It reads only the blank student copy and the raw transcript — never the answer key. It walks the transcript at speaking pace, as if hearing the sermon once with no rewind, and tries to fill in each blank. Then it reports which blanks it couldn’t get, which were ambiguous, where two blanks landed on top of each other, and where six-plus minutes of preaching gave it nothing to write.

sermon-editor is the opposite. It’s a careful theological editor with access to both the worksheet and the transcript. It checks every blank against what the preacher actually said, every scripture reference against the reading, flags verses that look reconstructed from memory instead of taken from the text, and surfaces anything that depends on an off-the-cuff line that probably won’t repeat at the 10am service.

Opus takes both reports and revises. The reviewers never agree on everything, which is the point. One optimizes for engagement in the pew. The other optimizes for not putting words in the preacher’s mouth. The draft that survives both is one I’d actually print.

The sheet is built from the 8:00 delivery but used at 10:00, so blanks go on things that repeat — main points, scripture as read, prepared one-liners, list items. Stories become short-answer questions instead of word-for-word blanks.

— The brief's main rule

The failure modes I actually care about

Most of the code in this project is about what to do when something goes sideways. A few that mattered:

  • No live stream by 8:30. The script falls back to looking for a completed replay of today’s 8am service on the channel. If neither shows up, it alerts me and gives up — a worksheet from last week’s sermon is worse than no worksheet.
  • yt-dlp stops finding the stream. YouTube breaks yt-dlp every few months. If a stream lookup fails twice in a row, the run self-updates it once (yt-dlp -U) before giving up. If that doesn’t help, I get a Slack DM and I’ll see it on my phone before the men’s group text thread wakes up.
  • The recorder dies mid-stream. Happens when network flaps. If the stream is still live and the recorder has exited, it restarts up to six times. Segment files survive restarts; transcription doesn’t care.
  • Opus times out or writes invalid JSON. There’s one quick repair pass without reviewers. If that fails too, the best valid draft on disk ships anyway. A rough worksheet is more useful than a missing one.
  • Ending gets cut off at 9:15. The worksheet ships with a note at the top: “The sermon was still going at my cutoff, so the last few minutes aren’t covered — use the Notes lines for the ending.” The reader knows what’s missing.

What I learned building it

The interesting part isn’t the pipeline — it’s that the whole system is designed around a quality the reader will never see: what repeats between the 8am and 10am service. The preacher gives the same message twice on Sunday. Same outline, same scriptures, same prepared one-liners. Different ad-libs, different jokes, different crowd. The worksheet has to be built from the first delivery and useful at the second, which means a bunch of things that look like fine blanks at 8:30 are actually traps.

Any time you read the “same” thing twice and you can sense what’s load-bearing and what’s incidental, that’s expensive. Baking that into a prompt and letting two agents argue about it is cheap. I think that’s the pattern I’ll keep reusing: one producer, two critics with opposite jobs, structured output in the middle so you can verify without re-reading.

The first live run is this Sunday. I’ll be in a tent at Garner State Park when the timer fires. If a PDF shows up in my Slack DMs by 9:20, the cron worked. If not, I’ll hear about it from the notification feed before anyone else in the campsite is awake.

Either way, the worksheet is going to be the first thing I pack for next Sunday.