pipeline / case study / skabene

A voice note becomes a vault note

A self-hosted capture pipeline: Telegram bot, n8n, Whisper, a vision model, Postgres, evening and night workflows, an Obsidian vault. Then the second half: the agent workflow that turned about 46 project folders into the vault the pipeline writes into.

01 Problem

Capture every thought by voice, text or photo with no friction, and get it back later as notes that can be searched and read. Both halves have to be honest: nothing lost on the way in, nothing invented on the way out.

self-hosted
n8n, Postgres and Whisper on one rented server; the vault on the home PC.
free tier
Chat models through a router on free-tier limits, about 50 requests a day.
one person
Built between 2026-08-28 and 2026-09-12, alone.
private data
The vault leaves the machine through Obsidian Sync, so every write obeys a redaction contract.

Everything on this page that looks like data is synthetic. No real capture, note or message is shown.

02 Architecture

Grouped left to right: capture, process, store, deliver, read. Node labels follow the workflow node names. The dashed edge is the error path.

Capture pipeline architecture Telegram bot to Route (auth guard, 13 commands). Voice goes to self-hosted Whisper, photos to a vision model with a no-invention guard, text straight to Postgres. Postgres feeds the 21:00 evening report and the 00:30 note generation, which writes to an inbox on the server. An hourly pull from the home PC stages, packs, verifies by MD5 and archives, and the notes land in the Obsidian vault, which AI tools read through MCP. Any workflow error goes to an error trigger that alerts a hardcoded chat. voice photo text ssh, pull-only Telegram bottext, voice, photo Routeauth guard, 13 commands Whisperself-hosted container Build Visionno-invention guard Postgres: capturesday = 04:00 rollover expenses, itemsreceipts 21:00 eveningreport, max 3 questions 00:30 generate notesvalidate, retry, empty server inboxdaily/, topics/ hourly pullstage, MD5, archive Obsidian vaultcapture/daily, topics AI toolsread over MCP errorTriggerany workflow Alert Mehardcoded chat
Hand-drawn SVG. Left out: the 07:30 morning brief, the calendar workflows and the review web app.

03 One note's journey

One synthetic voice note, step by step. The model outputs at steps 5 to 7 are real outputs from the re-enactment run below, on invented input. Everything else is built by the re-enactment script or written by hand, and each step says which.

    04 Re-enactment: what the run measured

    The production pipeline ran free-tier models inside n8n. This page does not show that system running. Instead tools/reenact.py replays the pipeline's two model steps on three synthetic days, with prompts written for this demo, against three Claude models through the Claude Code CLI. It repeats each call and applies the same kind of checks the Validate node applies.

    Per model and step. "Usable" means every check passed. Latency is wall-clock per CLI call, including CLI start-up, with 6 calls running at once.
    modelstepcallsusablefailed checkinvented datesfenced JSONlatency median (min to max), s

    Cost is not measured. The CLI calls here report no token counts, and none are estimated. The sample is small (3 repeats per day and model) and synthetic; read it as a smoke test of the guardrails, not a benchmark.

    05 Second story: the read path

    The pipeline wrote into a vault that nothing could read back. It was a good microphone with no memory. So the effort moved to the read side: on 2026-09-02 an agent workflow read about 46 project roots and wrote a structured vault that AI tools now read over MCP. It is agent orchestration with a human gate and verifiers, not a chatbot.

    Ingest workflow Workflow A fans out over about 46 roots to survey and harvest, reconciles into a plan and writes nothing. Two adversarial critics and a revision review the plan. A human approval gate cuts it. Workflow B fans out per bundle to write, verify and fix, under a redaction contract and a note checker. A second pass sends open findings through a fixer and a targeted verifier. ~46 rootsproject folders survey harvest harvest harvest … reconcileplan + 2 critics+ revision humangate bundlewrite/verify/fix bundlewrite/verify/fix …per project second passfixer + targeted verifieron open findings redaction contract + note checker on every write
    Shape and counts only. No screenshot of the real vault is shown; the vault is private.
    Counts from the ingest log and the approved plan
    project roots read~46Workflow A surveyed, harvested and reconciled; it wrote nothing.
    files read391Each logged with its sha256, so the next pass re-reads only what changed.
    plan review2 + 1Two adversarial critics and one revision, then a human approval gate.
    coverage rows88Each source covered or explicitly dropped with a reason.
    dropped44Planned notes cut by Workflow A, the critics and the owner at the gate.
    notes written93 + 3193 new, 31 updated, 124 in all. Updates append or patch; they never rewrite.
    bundles7574 went through a fix round; 73 finished with a high or medium verifier finding still open.
    second pass202Findings through a fixer and a targeted verifier on 2026-09-06; 17 bundles needed a second round; 72 of 73 closed clean.

    The part I'd point to is the verifier result: after the first write pass, 73 of 75 bundles still had an open finding. That is what the verify and fix steps are for; without them those findings would sit in the vault unnoticed. Key, wallet, credential and .env files are never read, by rule. The contract forbids renaming or moving a note. Two false positives in the note checker's redaction heuristics were fixed only with the owner's approval.

    06 Design decisions and trade-offs

    decisionwhycost
    Error alerts go to a hardcoded chatThe alert must still fire when Postgres is what broke.A config value lives in a workflow; changing it means editing the workflow.
    Unknown senders get no reply at allA Telegram bot is public; silence tells a stranger nothing.A misconfigured id also gets silence, which is harder to debug.
    captured_at = Telegram message timeA slow transcription once filed a note after shorter ones sent later.Insert order and day order can differ, so every query has to sort by it.
    Vision refuses tiny payloads; prompt forbids inventionWithout the guard the model invents a plausible receipt.A refused photo has to be sent again.
    Pull-only sync over SSH from the home PCNo open sync port, no relay; each run fetches only what is new.Notes arrive only when the PC is on.
    Differing collision is written beside the original as .regenerated-<runId>.mdA regenerated day must not overwrite hand edits in Obsidian.Duplicates to merge by hand.
    Remote scripts end with an __OK__ sentinelssh failures show only in the exit code; a dropped connection must not look like a quiet day.One more convention every remote script has to follow.
    Stage quiet files, verify each by MD5, archive lastn8n writes non-atomically and keeps writing during a pull.A note waits one extra run.
    Invented dates dropped; only days that produced a file are marked processedAn invented date could file notes under a day that never happened and leave the real day looking done.A dropped file means a day is regenerated.
    Evening check-in is report-first, at most 3 questions, plain textThree days of questions went unanswered; the prompt had been tuned for form. The Telegram node sends unparsed.Fewer questions, some worth asking go unasked.

    The "cost" column is my reading of each trade-off, not a measured figure.

    07 What broke, and how I found it

    2026-08-30: manual runs of the evening workflow seemed to hang on the Ask LLM step. I ruled out model ids, response_format support, server egress latency, timezone settings and apparent duplicate workflows. The model was never called. Get Day had returned zero rows, Build Prompt returned an empty array, and n8n skipped every downstream node. A skipped node renders greyed out, the same as one still spinning. The 0.35 s execution time gave it away.

    The fix, "Nothing captured" with no model call, landed in note generation on 2026-09-02. The evening workflow is still silent on an empty day. Silence that looks like failure is the bug I most want gone.

    08 Status

    In daily use from 2026-08-28 to 2026-09-03; paused by choice since.

    09 What I'd change

    1. Build the read path first. The chosen design is a push-only mirror of a named vault subset, next to the pull and over the same SSH, into a folder n8n can read. It was chosen over a Postgres index.
    2. Record the signal that cannot be backfilled. Add a questions table so each answer, skip or "useless" is recorded against the question it belongs to.
    3. Give the evening run the empty-day branch: one IF node, one Telegram line, no model call.
    4. Restore the two dropped guardrails and prove it with a receipt eval run before and after.
    5. Fire on accumulation, not the clock. The free-tier budget was named as the real design pressure, but no node counts requests.
    6. Sort in code, not in the prompt. From this page's re-enactment: models asked to list captures "in time order" put the after-midnight capture first. Rows already arrive in captured_at order, so code should emit the list and the model should only write prose. This fix has not been tested.

    10 Demo vault excerpt

    An invented vault for a fictional user, in the same folder shape. The files under capture/ are the sample model output from the re-enactment. The project notes are made up for illustration.

    
        

    11 Sources

    Each claim about the real pipeline and the ingest, with the note in my private vault it comes from. The vault is not published. Re-enactment numbers come from data/reenactment.json.