Skip to main content
The useful part of a meeting is who agreed to do what by when, and it is usually lost by the afternoon. This pipeline keeps the timestamps from the transcript, has a model extract each action item with the exact words that created it and a real date resolved from “by Thursday”, and writes the email from that checked JSON with a template, so nothing in the email was invented by a second model. It costs 1.2 cents for a 34-second recording, all of it the transcription; the extraction runs on the free tier.

Build it in your project

Paste this line into your agent (Claude Code, Codex, Cursor, OpenCode, Cline). It sets up your key, then builds the pipeline in your project:

The code

One file per language. Both read RUNCRATE_API_KEY from the environment; nothing secret is in the code.

What the proof run cost

The Python file on a 34-second three-person launch sync dated 2026-10-19 (a Monday). Measured on 2026-10-23 against api.runcrate.ai; costs are what the account was billed, read from each response’s X-LLM-Cost header or usage.cost. Both runs found the three action items with the right due dates (“by Thursday” became 2026-10-22) and the Friday 10:00 follow-up. The TypeScript run, also $0.0120, attributed Priya’s item to “Unidentified speaker”.

Scale it

  • Every meeting is independent: run it from your recorder’s upload webhook.
  • Hours of audio: transcription is $0.021 a minute with whisper-large-v3-turbo; Voxtral Small is $0.003 a minute but gives no timestamps.
  • Long meetings fit: DeepSeek V4.1 Flash takes a million tokens of context.

Gotchas found in the proof run

  • Transcripts carry no speaker labels, so attribution depends on people saying names. One of two runs missed an owner who only introduced herself at the start (“Priya here”). If it matters, diarise first or ask people to name owners.
  • DeepSeek V4.1 Flash took 34 s for the extraction: it reasons before answering. gpt-oss-120b is faster if you need minutes in seconds.
  • Strict JSON schemas with nullable fields ("type": ["string", "null"]) worked on both runs.