Skip to content
Abdullah.
All work

Case study · December 2025

Brain — pulling your own commitments out of a meeting transcript

A meeting transcript contains commitments you made, and they evaporate unless you write them down. Brain extracts only yours, resolves the dates deterministically, and keeps the source text even when the model fails.

Role
Solo — design and build
Timeframe
December 2025
Access
Public source · demo needs a GitHub sign-in

No public demo

The hosted app is behind a GitHub sign-in, so there is nothing to see without an account. The source is public and is the better read.

Brain Assistant landing screen on a near-black background: the product name, the line “a memory prosthetic for commitments made in conversation”, and a sign-in card offering GitHub authentication.

At a glance

Problem

Things you agree to in meetings disappear the moment the call ends. Transcript summarisers give you back a summary of the whole meeting — including everyone else’s action items — which is not the same as a list of what you now owe someone.

Approach

Treat the model as one unreliable step in a pipeline rather than the pipeline itself. It gets a single narrow job: identify first-person commitments and hand back the date phrase verbatim. Everything that can be done deterministically — date resolution, persistence, ownership — is done in code around it.

Outcome

Deployed and in personal daily use. The transcript-to-task loop works end to end: paste, extract, store, list by due date. Early versions shipped with public database policies; those were replaced with per-user rows and row-level security scoped to the authenticated user.

Stack

  • Next.js 16 (App Router)
  • React 19
  • TypeScript
  • Vercel AI SDK
  • OpenAI GPT-4o-mini
  • Zod
  • chrono-node
  • Supabase (Postgres, Auth, RLS)
  • Tailwind CSS v4
  • Vercel

Decisions

4 choices where the alternative was reasonable. The rejected option is the interesting half.

  1. Rejected
    Asking the model for prose and parsing it
    Chosen
    generateObject against a Zod schema
    Because
    Free text means a regex, and a regex means a silent failure the first time the model phrases something differently. A schema makes the output a typed object or an error — there is no third state to handle later.
  2. Rejected
    Letting the model resolve "next Friday"
    Chosen
    Model returns the date phrase verbatim; chrono-node resolves it against a reference date
    Because
    An LLM asked to do calendar arithmetic will answer confidently and be wrong often enough to matter, and you cannot tell which answers are the wrong ones. Extraction is a language problem, so the model does that; date resolution is arithmetic, so code does that.
  3. Rejected
    Extracting every action item in the transcript
    Chosen
    Prompt parameterised with the user’s name and the aliases they go by
    Because
    A list of everyone’s commitments is a meeting summary, and there are plenty of those. The useful artefact is the short list of things you said you would do — which means the extractor needs to know who you are and how people refer to you in a transcript.
  4. Rejected
    Failing the request when extraction fails
    Chosen
    Return no tasks, but always persist the raw note
    Because
    Losing the source text is worse than losing the tasks. If the model call fails, the transcript is still saved and can be reprocessed; the alternative loses the only copy of the input to an upstream error.

The problem

You agree to things in meetings. "I'll send that over by Friday." "Let me pick that up." Those commitments are real, and they exist for about four seconds before the call ends and they're gone.

Transcripts are free now — Zoom and Meet generate them automatically. So the raw material is sitting there. The gap is that a transcript summariser gives you back a summary of the meeting, including every action item assigned to everyone in it. That is not the thing you need. The thing you need is the short list of what you now owe someone.

The pipeline

Paste text, get tasks. Four steps, and only one of them involves a model.

  1. Persist the raw text first. The note is written to Postgres before extraction runs.
  2. Extract, with a schema. generateObject from the Vercel AI SDK against a Zod schema, using GPT-4o-mini. The model returns an array of { title, description, due_date_raw } — typed objects, not prose.
  3. Resolve dates in code. due_date_raw comes back as whatever the speaker said: "tomorrow", "next Friday", "15th December". chrono-node turns that into a real date against a reference of now, and it's normalised to YYYY-MM-DD before it touches the database.
  4. Store the tasks against the note, scoped to the user, sorted by due date.

The decisions that made it work

The model does one language job and nothing else. Its brief is: find first-person commitments, and hand back the date phrase exactly as it was said. It is explicitly not asked what date "next Friday" is. Asking a language model to do calendar arithmetic gets you a confident answer that is wrong often enough to matter, and — worse — gives you no signal about which answers are the wrong ones. Splitting it means the failure modes are separable: if the extraction is bad, that's a prompt problem; if the date is bad, that's a parser problem with a reproducible input.

It needs to know who you are. The prompt is parameterised with the user's name and the aliases they get called in a transcript, including bare first-person "I'll". Without that, extraction returns everyone's action items and the output is just a worse meeting summary. With it, the tool has an opinion about what belongs to you, which is the entire product.

Failure degrades instead of cascading. If the model call throws, the extractor returns an empty array rather than propagating. The note is already saved, so the input survives and can be reprocessed. The alternative — failing the whole request — loses the only copy of a transcript to a transient upstream error, which is a much worse outcome than a note with no tasks attached.

Constrained output, so there's nothing to parse. Because extraction is schema-validated, there's no regex, no "sometimes it wraps the JSON in a code fence", no branch handling the case where the model got chatty. Output is either a valid typed object or an error.

Security, honestly

The first version stored notes and tasks with no owner column and public database policies. That was fine for a single-user prototype and would have been a real problem the moment anyone else used it.

Fixing it meant a migration that added user_id to both tables with a cascade to auth.users, backfilled the existing rows, made the column NOT NULL, enabled row-level security, and wrote policies scoping select, insert, update and delete to auth.uid(). There is a migration in the repo literally called remove_insecure_public_policies.sql. I'd rather that be visible than quietly squashed — the useful part of the story is noticing, not never having done it.

Every write path checks the session server-side too. The API route resolves the authenticated user before it touches anything and returns a 401 if there isn't one, so RLS is the backstop rather than the only line.

What I'd do next

Source integrations, so transcripts arrive instead of being pasted — that was always the point, and pasting is the deliberately unambitious v0. Beyond that, the interesting problem is evaluation: right now I can tell you the pipeline runs, but I can't tell you what fraction of my actual commitments it catches.