Build AI Meeting Intelligence: From Recordings to Decisions, With Cost Per Meeting and Failure Modes (2026)
- A 45-minute meeting costs about 83 cents to turn into verified decisions with owners and dates on the arithmetic below — and only 16 cents of that is the language model. Capture and transcription are 79% of the bill, which inverts every instinct you have about where to optimise.
- Word error rate is not your problem. Diarisation error rate is. Published benchmarks put commercial diarisation at 8-14% error on meeting audio and 25-40% on overlapping speech, which means roughly one action item in eight is attributed to the wrong person if you write owners automatically.
- That single fact sets your entire budget. If a human confirms every write, 12% diarisation error is survivable and the meeting costs 83 cents. If you auto-write owners into a task tracker, you need per-participant audio channels, and the meeting costs about $1.72. The write path chooses the microphone.
- 11 · Capture with consent$0.375 + $0.038 storage
A bot joins, or a recording SDK runs on the host machine. Consent state is recorded per participant and per jurisdiction before a single byte is stored. This is a legal boundary, not a settings page.
- 22 · Transcribe and diarise$0.2835 · 8-14% DER
Words with timestamps, plus speaker turns. Diarisation produces anonymous labels — speaker 0, speaker 1 — and knows nothing about who those people are.
- 33 · Resolve speakers to people$0.0075
Map anonymous turns to real attendees using the calendar invite, the join log and voice prints. The attendee list bounds the label space, which is why this is cheap and effective.
- 44 · Extract typed items$0.072
Decisions, action items, owners, dates, risks and open questions — each as a structured record carrying the verbatim transcript span that justifies it. Never free prose.
- 55 · Verify before writing$0.015
Every span must literally appear in the transcript. Every named owner must be an actual attendee or a known colleague. Every date must resolve to a real calendar date. Deterministic, mostly free.
- 66 · Write through MCP$0.010
Tasks into the tracker, notes into the CRM, decisions into the doc store — idempotent, attributed and reversible, with everything below the confidence threshold queued for a human.
- 77 · Index for retrieval$0.0002
Chunk embeddings plus an entity graph over people, projects and decisions, so that six months later somebody can ask when did we decide to drop the Postgres migration and get an answer with a timestamp.
What is AI meeting intelligence, architecturally?
A capture layer, a transcription and diarisation stage, a speaker-resolution step that maps anonymous voice labels to real people, a typed extraction pass with verbatim span citations, a verification gate, and MCP writes into the systems where work actually lives. Six stages. Only one of them is a language model problem.
That is the opposite of how the category is usually described. Meeting AI is sold as summarisation, which is the least valuable stage in the pipeline and the one every vendor has already commoditised. Nobody in an organisation has ever been blocked by not having a summary. They have been blocked by a decision that was made in a room they were not in, an action item that was assigned to nobody, and a commitment to a customer that never reached the CRM. The product is the write path, not the read path.
The scope worth targeting is therefore narrow and mechanical: turn each meeting into a set of typed records — decision, action item, owner, due date, risk, open question — each with a verbatim quote and a timestamp, then land them in the tracker and the CRM idempotently, and index everything so that history is queryable. Summaries fall out for free and should be treated as a by-product rather than the deliverable.
This is a reference design: how I would build it, and what the arithmetic says it costs. It draws on architecting AccioMatrix — an event-driven AI assessment and interview platform orchestrating multiple LLMs, agents and MCP tooling for 20+ enterprise clients — and on building a custom real-time voice stack at roughly 2.5 cents a minute, which is where I learned how much of a speech product's bill has nothing to do with the model. I have not shipped a meeting intelligence product in production. Every figure below is modelled with its inputs printed.
One structural fact shapes everything downstream. In a coding agent, described in the PR-shipping coding agent build, models are 3% of the cost and human review is the rest. Here, models are 16% of the cost and audio infrastructure is the rest. Same studio, same patterns, opposite cost centre — which is why cost-per-request arithmetic has to be done per product rather than assumed from a category.
| Component | What it does | Implementation | Cost per 45-min meeting | Primary failure mode | Skip in v1? |
|---|---|---|---|---|---|
| Capture + consent | Joins the call or records on the host, stores per-participant consent state before any audio is retained | Meeting-bot API or a desktop recording SDK, modelled at $0.50 per recording hour | $0.375 + $0.038 storage | Records a jurisdiction requiring all-party consent without capturing it — a legal problem, not a bug | No |
| Transcription | Words with per-word timestamps, punctuation, and a confidence per token | Batch ASR at a modelled $0.0043 per minute | $0.194 (in the line below) | Domain vocabulary — product names, customer names, acronyms — transcribed as near-homophones | No |
| Diarisation | Anonymous speaker turns: who spoke when, without knowing who they are | Vendor diarisation at a modelled $0.002 per minute on top of ASR | $0.2835 combined | 8-14% error on clean meeting audio, 25-40% on overlapping speech, per published benchmarks | No |
| Speaker resolution | Maps anonymous turns to named attendees using calendar, join log and voice prints | Haiku-class model constrained to the attendee list, plus deterministic join-log matching | $0.0075 | Confidently names the wrong attendee, which silently corrupts every owner downstream | No — this is the wedge |
| Typed extraction | Decisions, actions, owners, dates, risks — each with a verbatim span | Sonnet-class, 3 overlapping chunks plus one consolidation pass, schema-validated | $0.072 | Invents an action item from a hypothetical — we could ask Legal becomes a task | No |
| Verification gate | Span must exist verbatim, owner must be a real person, date must resolve | Deterministic string and directory matching plus a cheap-model paraphrase check | $0.015 | Passes a paraphrase that inverts the meaning of a decision | Absolutely not |
| MCP write gateway | Idempotent, attributed, reversible writes into tracker, CRM and doc store | MCP servers behind a gateway, ~8 tools, one idempotency key per extracted item | $0.010 | Re-processing a meeting creates duplicate tasks; the tracker becomes noise and adoption dies | No |
| Meeting index | Chunk embeddings plus an entity graph over people, projects and decisions | pgvector plus BM25, with an ACL column derived from the attendee list | $0.0002 | A permissionless index lets anyone retrieve a compensation discussion they were not in | Index: no. Entity graph: month two |
| Run journal | Every stage, model call, token count, dollar and confidence score, queryable | Postgres plus OTel GenAI spans | ~$0 | Cannot explain six weeks later why a task was created and assigned to someone | No |
What does the full architecture look like?
Twelve components across four columns: capture and directory context, transcription and speaker resolution, extraction and verification, and the write and retrieval surfaces. The load-bearing detail is that verification sits between extraction and every write, and that the calendar feeds speaker resolution rather than the extractor.
Start with capture, because it is where the legal and product constraints are both hardest. Two viable approaches exist and they differ in more than implementation. A bot that joins the call is universal across platforms, visible to everyone in the room, and produces a mixed-down mono stream — which forces you into diarisation. A recording SDK on the host machine can capture per-participant streams, which eliminates most diarisation error, but it requires deployment to endpoints and it is less obviously visible to other participants, which is a consent design problem before it is an engineering one. Choose deliberately; this decision propagates into your accuracy ceiling and your bill.
The calendar and directory feed is the cheapest quality lever in the entire system and the one most designs omit. Diarisation gives you anonymous labels. The calendar gives you a bounded list of five candidate humans, their email addresses, their roles and their reporting lines. Resolving speaker 2 to a name against a five-person candidate set is a fundamentally easier problem than open-set speaker identification, and it costs less than a cent. It also gives you the directory check that stops the extractor assigning an action item to a person who was never in the room.
Extraction runs over overlapping chunks rather than the whole transcript in one call, and the overlap is not an optimisation — it is correctness. Decisions in meetings are frequently split across a chunk boundary: the proposal at minute nineteen, the objection at minute twenty-two, the agreement at minute twenty-six. A chunk window with a two-minute overlap plus a consolidation pass that deduplicates by verbatim span catches those. A single 10,000-token call is cheaper and quietly loses the decisions that took the longest to reach, which are the ones that mattered.
Everything to the right of verification is deliberately boring plumbing: idempotent writes with a key derived from the meeting id and the item's span hash, a human confirmation queue for anything below threshold, and an index with an ACL column. The gateway pattern for the write surface is the same one described in MCP in production, and the retrieval side follows the permission-aware approach in RAG over private documents.
How accurate does the transcription and diarisation need to be?
Accurate enough that owner attribution is trustworthy, which is a much higher bar than accurate enough that the summary reads well. Word error rate in the 5-10% band is fine for extraction because the language model absorbs it. Diarisation error is not absorbed by anything — a wrong speaker label becomes a wrong owner, silently.
The asymmetry is worth stating plainly. If your transcript renders a product name wrong in three places, the extractor still finds the decision and the summary still reads correctly; a reader notices the typo and moves on. If diarisation swaps two speakers during a fast exchange, the extractor faithfully records that Priya committed to the migration when it was actually Daniel, attaches a verbatim span that genuinely appears in the transcript, passes every verification check you have built, and writes a task to the wrong person. Every downstream safeguard in this pipeline is blind to that failure, because the failure happened before the text existed.
The published numbers set the expectation. Benchmark write-ups put pyannote.audio 3.1 at roughly 11% diarisation error rate on the AMI meeting corpus, with commercial APIs from the major vendors reported in the 8-14% band on comparable meeting audio. The figure that matters more for real calls is overlapping speech, where reported error rates sit in the 25-40% range — and overlap is not an edge case in a five-person meeting, it is the texture of every genuine disagreement, which is where the decisions are.
There are three honest ways to buy accuracy, and they cost very different amounts. The cheapest is the directory constraint already described: bounding speaker labels to the calendar attendee list, which costs under a cent and converts an open-set problem into a five-way classification. The middle option is a premium ASR configuration — keyword prompting with your product vocabulary, redaction, better acoustic models — which on the modelled vendor rates takes a fully featured stack to roughly $0.0130 per minute and adds about 30 cents to a 45-minute meeting. The expensive option is per-participant audio channels, which sidesteps diarisation almost entirely and roughly doubles the cost of the meeting.
Choose by write path, not by taste. A system where a human confirms each item can run on mono audio with vendor diarisation and the directory constraint, at about 83 cents a meeting. A system that writes owners into a tracker automatically should be on per-participant channels at about $1.72, and should still hold back any item whose speaker confidence is below threshold. At 4,400 meetings a month those two designs are roughly $3,650 and $7,560 — a difference of about $3,900 a month, bought entirely to remove a class of error that no downstream check can catch.
| Configuration | Modelled ASR cost per 45-min meeting | Total cost per meeting | Expected speaker accuracy | When it is the right choice |
|---|---|---|---|---|
| Mono, no diarisation | $0.194 | $0.65 | None — no owner attribution possible | Never for this product. Fine for a pure transcript archive |
| Mono + vendor diarisation | $0.284 | $0.83 | 8-14% DER on clean audio, 25-40% on overlap | Human confirms every write. The sensible default for v1 |
| Mono + diarisation + directory constraint | $0.284 + $0.0075 | $0.83 | Materially better than raw DER — five-way choice, not open set | Always. This is under a cent and there is no reason to skip it |
| Premium ASR stack (keyterms, redaction) | $0.585 | $1.13 | Better word accuracy on domain vocabulary; DER largely unchanged | Heavy jargon, customer names, regulated content requiring redaction |
| Per-participant channels | ~$1.17 (225 channel-minutes) | $1.72 | Diarisation effectively eliminated outside true overlap | You write owners into a tracker without human confirmation |
What happens to one meeting, end to end?
The bot leaves, the recording finalises, batch ASR with diarisation runs, speakers resolve against the calendar, three overlapping chunks are extracted in parallel and consolidated, every item is verified against the transcript, high-confidence items are written through MCP and the rest are queued, and the transcript is embedded. Roughly four minutes of wall clock after the call ends.
Batch rather than streaming is the right default and it saves real money. Streaming ASR at the modelled vendor rates is roughly 80% more expensive per minute than batch, and the only thing it buys is in-meeting output — a live agenda tracker or an interruption that says that sounds like an action item. That is a genuinely different product with a genuinely different value proposition, and it should be a deliberate second phase rather than an accidental architecture decision made because streaming demos better.
The four-minute delay is also a product feature if you use it. It gives you a window to run the consolidation pass properly, to look up the entities mentioned against your CRM and tracker, and to check whether an action item duplicates one created in last week's meeting on the same project. A system that posts a summary eleven seconds after the call ends and creates a duplicate of an existing ticket has optimised the wrong variable.
The idempotency design deserves attention because reprocessing is routine here, not exceptional. Meetings get re-transcribed when the vendor improves a model, when a customer disputes an extraction, when a chunk boundary bug is fixed. The key for each write should derive from the meeting id, the item type and a hash of the verbatim span — not the item's text, which the model will phrase slightly differently on every run. With a span hash, re-extracting the same meeting produces the same keys and creates zero duplicates. With a text hash, it produces a second copy of every task, and the tracker becomes noise within a fortnight.
Note where cost concentrates in the sequence. Capture and ASR complete before the first language model call, and by then 79% of the meeting's cost is already spent. Every model call after that point is measured in cents or fractions of cents. This is why prompt-level cost optimisation is close to pointless in this product and why the two levers that matter are recording-hour hygiene — do not record meetings nobody will ever query — and the choice of ASR configuration.
$ $ meetingctl show-item mtg_8f2a --id itm_04type action_itemtitle Send the revised SOC 2 scope to Northwind before Fridayowner_resolved daniel.okafor@example.com (attendee, join_log match)owner_confidence 0.94due_date 2026-08-28 (resolved from "before Friday", meeting date 2026-08-24)span_start 00:31:12.480span_text "I'll get the revised scope over to Northwind before Friday"span_verified true (exact substring of transcript segment 214)speaker_turn turn_211 diarisation_label spk_2 overlap_flag falseidempotency_key mtg_8f2a:action_item:sha256(span)[0:16] = 7c1f9ab3e40d2286gate_score 0.88 -> auto_writewritten_to linear:ENG-4417 (created 2026-08-24T14:07:02Z, reversible)$ $ meetingctl show-item mtg_8f2a --id itm_07type action_itemtitle Ask Legal whether the DPA needs re-signingowner_resolved null (no first-person commitment in span)span_text "someone should probably ask Legal about the DPA"gate_score 0.41 -> human_confirm_queue (reason: unowned_hypothetical)
How do you extract decisions, owners and dates without inventing them?
By making the schema do the work. Every extracted item must carry a verbatim transcript span, an owner drawn from a closed set of real people, and a date that resolves against the meeting date. Anything that cannot supply all three is not softened or guessed — it is dropped into a human queue with the reason recorded.
The failure this prevents is specific and extremely common. Meetings are full of conditional language: we could ask Legal, maybe someone should look at the Postgres numbers, if that slips we would need to talk to the customer. A model asked to extract action items will convert a good proportion of those into tasks, because they are shaped exactly like tasks. The rule that eliminates most of it is not a prompt instruction but a schema constraint plus a deterministic check: an action item requires a first-person or directly addressed commitment in its span. That is a string-level property, it is checkable, and it does not depend on the model agreeing with you.
Owner resolution should be a closed-set lookup, never free text. The extractor emits a speaker turn id and a claimed owner name; the verifier resolves both against the attendee list and the company directory. If the claimed owner is not an attendee and not a directory match, the item keeps its content and loses its owner, and it goes to the queue. That sounds pedantic until you consider the alternative, which is a tracker slowly filling with tasks assigned to a person whose name the model half-heard.
Date resolution is the third leg and it is the one that quietly produces wrong data. Before Friday, next sprint, end of quarter and in two weeks all resolve against the meeting date, in the meeting's timezone, using a deterministic parser — not a model. Models are unreliable at relative date arithmetic in a way that is easy to miss, because the answer is always plausible and only sometimes right. Pass the meeting date and timezone into a parser, and have the verifier reject any date the parser cannot produce.
The consolidation pass is where duplicates die. Three overlapping chunks will each independently extract the decision that was discussed across their shared boundary, in three slightly different phrasings. Deduplicate on the verbatim span rather than the generated title, because the spans are identical and the titles never are. This one choice is the difference between nine clean items and fourteen items where five are near-duplicates, and near-duplicates in a task tracker are indistinguishable from a broken product.
import { z } from "zod";
export const ExtractedItem = z.object({
type: z.enum(["decision", "action_item", "risk", "open_question"]),
title: z.string().min(6).max(160),
span_text: z.string().min(15), // must appear verbatim in the transcript
span_turn_id: z.string(), // which diarised turn it came from
claimed_owner: z.string().nullable(), // resolved later against a closed set
claimed_due: z.string().nullable(), // relative language is fine here
});
export type Item = z.infer<typeof ExtractedItem>;
type Turn = { id: string; speakerEmail: string | null; text: string; overlap: boolean };
type Context = {
turns: Map<string, Turn>;
attendees: Set<string>; // emails on the calendar invite
directory: Set<string>; // everyone in the company
meetingDateIso: string;
timezone: string;
};
export type Verdict =
| { ok: true; owner: string | null; dueIso: string | null; score: number }
| { ok: false; reason: string };
const COMMITMENT = /\b(i(?: will|'ll| can| am going to)|we(?: will|'ll)|let me|i'm going to)\b/i;
export function verify(item: Item, ctx: Context): Verdict {
const turn = ctx.turns.get(item.span_turn_id);
if (!turn) return { ok: false, reason: "unknown_turn" };
// 1. free: the span must literally exist in the turn it claims to come from
if (!norm(turn.text).includes(norm(item.span_text))) {
return { ok: false, reason: "span_not_found" };
}
// 2. free: an action item needs an actual commitment, not a hypothetical
if (item.type === "action_item" && !COMMITMENT.test(item.span_text)) {
return { ok: false, reason: "unowned_hypothetical" };
}
// 3. free: owners come from a closed set. Never accept free text.
let owner: string | null = null;
if (item.type === "action_item") {
const candidate = item.claimed_owner ?? turn.speakerEmail;
if (!candidate) return { ok: false, reason: "no_owner" };
if (!ctx.attendees.has(candidate) && !ctx.directory.has(candidate)) {
return { ok: false, reason: "owner_not_a_real_person" };
}
owner = candidate;
}
// 4. free: dates are parsed deterministically against the meeting date
let dueIso: string | null = null;
if (item.claimed_due) {
dueIso = resolveRelativeDate(item.claimed_due, ctx.meetingDateIso, ctx.timezone);
if (!dueIso) return { ok: false, reason: "date_unresolvable" };
}
// Speaker confidence is the term that decides auto-write. Overlapping
// speech is where diarisation fails, so it costs the item its autonomy.
const score = turn.overlap ? 0.55 : 0.9;
return { ok: true, owner, dueIso, score };
}
function norm(s: string): string {
return s.toLowerCase().replace(/[^a-z0-9 ]/g, "").replace(/\s+/g, " ").trim();
}The clean case, and on the modelled mix roughly three items in four. Written idempotently on the span hash, attributed to the meeting, and reversible with one click.
Overlap is where diarisation error rates jump to the 25-40% band reported in published benchmarks. The content is probably right and the owner is a coin flip, so a human picks the name.
Decisions do not need owners. Recording them in a searchable log is most of this product's long-term value and carries almost none of its risk.
The single most common invented task. Showing the user the exact phrase that failed the check teaches them the system is careful rather than broken.
The model fabricated or paraphrased. Never show this to a user and always count it — the rate of span-not-found is your cleanest hallucination metric and it needs no labelling.
How does the agent write into the task tracker and CRM safely?
With about eight MCP tools, every write idempotent on a key derived from the meeting id and the item's span hash, every created object attributed back to its meeting and its verbatim quote, and every write reversible by the person it names. No bulk operations, no updates to objects the meeting did not create, and no deletes.
Idempotency is not a nice-to-have in this pipeline because reprocessing is normal. A vendor ships a better diarisation model and you re-run last month. A user disputes an extraction and you re-run one meeting. A chunk-boundary bug is fixed and you backfill. Each of those must produce zero duplicate tasks, and the only way to guarantee that is a deterministic key computed from something stable. The span hash is stable; the generated title is not, because the model rephrases it every run. Get this wrong and the failure is not an error message — it is a tracker with three copies of everything and a team that stops trusting it.
Attribution is the second requirement and it is what makes the system defensible in an argument. Every created task should carry the meeting, the timestamp, the speaker and the verbatim quote that produced it, rendered in the task description. When someone says I never agreed to that, the answer is a link to thirty-one minutes and twelve seconds of a recording and a sentence in their own words. That is worth more to adoption than any accuracy improvement, because it converts disputes from opinion into evidence.
The tool surface should be deliberately anaemic. Create a task, add a comment, log a call note, attach a decision to a project, look up a person, look up an existing task to avoid duplicates, and read the tracker's field schema. That is seven reads and writes that cannot damage anything a human did not ask for. Explicitly absent: closing tasks, reassigning existing tasks, editing CRM opportunity stages, and anything that touches an object created by a person rather than by the pipeline. A meeting tool that edits your colleague's tickets is a meeting tool that gets banned.
On transport, MCP is a good fit here precisely because the targets are third-party systems you did not build. The July 2026 specification revision removed protocol-level sessions and the Mcp-Session-Id header from Streamable HTTP, which means a tracker MCP server is now an ordinary stateless HTTP workload you can scale behind a load balancer — useful when a Monday morning backlog of four hundred meetings all want to write at once. Rate-limit per tool name at the gateway using the Mcp-Name header rather than parsing bodies.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createHash } from "node:crypto";
import { z } from "zod";
const server = new McpServer({ name: "tracker", version: "3.0.1" });
server.registerTool(
"create_task",
{
title: "Create a task from a verified meeting action item",
description:
"Creates one task in the tracker from an action item that has already " +
"passed verification. The task is attributed to its meeting and quotes " +
"the transcript verbatim. Safe to retry: an identical item returns the " +
"task created the first time. Do NOT call this for decisions or risks - " +
"use write_decision, which does not create work for anyone.",
inputSchema: {
meetingId: z.string(),
spanText: z.string().min(15),
spanTimestamp: z.string(),
title: z.string().min(6).max(160),
ownerEmail: z.string().email(),
dueIso: z.string().nullable(),
projectKey: z.string(),
},
},
async (args, extra) => {
// Workspace comes from the verified request context, never the model.
const workspaceId = requireWorkspace(extra.requestInfo);
// Key is derived server-side. A client cannot weaken idempotency by
// choosing a worse key, and re-extraction produces the same span hash.
const spanHash = createHash("sha256").update(norm(args.spanText)).digest("hex").slice(0, 16);
const idempotencyKey = args.meetingId + ":action_item:" + spanHash;
const existing = await tracker.findByIdempotencyKey({ workspaceId, idempotencyKey });
if (existing) {
// A retry is normal. Return the original, do not create a second task.
return ok({ taskId: existing.id, created: false, url: existing.url });
}
const owner = await directory.resolve({ workspaceId, email: args.ownerEmail });
if (!owner) {
return ok({ error: "owner_not_in_workspace", taskId: null, created: false });
}
const description = [
args.title,
"",
"> " + args.spanText,
"",
"Said at " + args.spanTimestamp + " in meeting " + args.meetingId + ".",
"Created automatically. If this is wrong, click Reject and it will be removed.",
].join("\n");
const task = await tracker.createTask({
workspaceId,
projectKey: args.projectKey,
title: args.title,
description,
assigneeId: owner.id,
dueDate: args.dueIso,
idempotencyKey,
source: { kind: "meeting", meetingId: args.meetingId, spanHash },
});
return ok({ taskId: task.id, created: true, url: task.url });
},
);
function ok(payload: unknown) {
return { content: [{ type: "text" as const, text: JSON.stringify(payload) }], structuredContent: payload };
}
function norm(s: string): string {
return s.toLowerCase().replace(/\s+/g, " ").trim();
}| Kind | Scope | Idempotent | Blast radius if wrong | |
|---|---|---|---|---|
| create_task | write | one project, one workspace | yes — meeting id + span hash | A task assigned to the wrong person. Common, visible, one-click reversible |
| write_decision | write | decision log only | yes | A wrong entry in a searchable log. Low harm, high long-term value |
| log_call_note | write | one CRM record, notes only | yes | A note on the wrong account — check the account resolution, not the model |
| add_comment | write | objects this pipeline created | yes | Comment noise. Deliberately cannot comment on human-created objects |
| find_similar_task | read | one project | n/a | Misses a duplicate; the tracker gains a near-copy, which users read as broken |
| lookup_person | read | workspace directory | n/a | Owner resolution fails closed and the item goes to the queue. Correct behaviour |
| read_field_schema | read | one project | n/a | A malformed create call, caught by validation before it reaches the tracker |
| read_account | read | CRM, tenant-scoped | n/a | Wrong account context in a note; the serious one if it crosses a customer boundary |
| close_task / reassign_task | not exposed | n/a | n/a | Deliberately absent. A meeting tool that closes your colleague's tickets gets banned |
| update_opportunity_stage | not exposed | n/a | n/a | Deliberately absent. Pipeline stage is a human judgement with revenue attached |
How do you retrieve across months of meeting history?
With hybrid search over transcript chunks, an entity graph linking people to projects to decisions, an ACL column derived from the attendee list, and a recency-aware ranking that knows a decision can be superseded. The hard part is not retrieval quality. It is permissions and supersession.
Permission-aware retrieval is non-negotiable and it must be enforced at query time in the same store as the data. A meeting index is one of the most sensitive corpora a company will ever build: compensation discussions, performance conversations, legal strategy, acquisition talks. The rule is that a chunk is retrievable by the people who were in that meeting plus anyone explicitly granted access, and that filter is a predicate in the query, not a post-filter over results. Post-filtering leaks through result counts, through latency, and eventually through a bug.
Supersession is the subtler problem and it is what makes meeting history different from document retrieval. Documents get edited; meetings do not. If a team decided in March to use Postgres and decided in June to use DynamoDB, both meetings are permanently true records of what was decided at the time, and a naive similarity search will happily return the March one because it discussed the topic at greater length. The fix is a decision entity that carries a topic key, so that the June decision explicitly supersedes the March one and the retrieval layer can say the current decision is DynamoDB, decided 12 June, superseding a 4 March decision to use Postgres.
That single behaviour is most of the durable value of the whole product. Summaries decay in usefulness within a week. A queryable, permission-aware, supersession-aware record of what this company decided and when is an asset that compounds, and it is the thing no per-seat transcription tool gives you because it requires an entity model rather than a search box.
Cost here is almost nothing, which is worth saying because teams often assume retrieval is the expensive part. Embedding a 10,000-token transcript at the modelled rate is $0.0002. A year of 4,400 meetings a month is roughly 53,000 meetings, about 530 million tokens, and about $10.60 to embed in total. The retrieval agent itself — a question, a hybrid search, one grounded answer with citations — is a few cents per query on a cheap model. The architecture for this half of the system is the same one described in RAG over private documents; what is specific here is the ACL derivation and the supersession graph.
A chunk is visible to the meeting's attendees plus explicit grants. Enforced as a predicate inside the query, in the same store as the data, never as a post-filter over returned results.
$0 · fails: a leak nobody detectsBM25 plus vector over transcript segments, chunked on speaker turns rather than fixed token windows so that a chunk is always somebody saying something complete.
$0.0002/meeting to embedPeople, projects, customers, decisions and the edges between them, built from extraction output rather than from the raw text. This is what makes what did we decide about X answerable.
month two · compounding valueDecisions carry a topic key. A newer decision on the same topic supersedes the older one, and the answer says so explicitly with both dates. Documents get edited; meetings never do.
the durable moatEvery sentence in the answer cites a meeting, a timestamp and a speaker. No citation, no sentence. A confident uncited answer about a decision is worse than no answer.
~$0.03/query- Returns the March meeting because it discussed Postgres at greatest length
- No notion that a June meeting reversed the decision
- Answer is fluent, cited, and describes a decision that no longer holds
- Users discover the error in a planning meeting, in front of people
- Decisions carry a topic key and a supersedes edge
- Answer states the current decision, its date, and what it replaced
- Both meetings remain retrievable as historical record — nothing is deleted
- Requires typed extraction from day one, which you are building anyway
What does one meeting actually cost?
About 83 cents for a 45-minute, five-person meeting on mono audio with vendor diarisation, of which 45% is the recording bot, 34% is transcription and diarisation, and 16% is every language model call in the pipeline combined. At 4,400 meetings a month that is roughly $3,650 in variable cost.
Work the line items. Capture at the modelled $0.50 per recording hour is $0.375 for 45 minutes, plus about 3.8 cents of storage. Batch ASR at $0.0043 per minute plus diarisation at $0.002 per minute is $0.2835. Speaker resolution on a cheap model is three quarters of a cent. Extraction — three overlapping chunks at 5,000 in and 700 out each, plus a consolidation pass — is 7.2 cents on Sonnet-class pricing. Verification is 1.5 cents, the summary and per-attendee recap 3.2 cents, tracker field mapping one cent, and embeddings two hundredths of a cent. Total $0.8327.
The build-versus-buy arithmetic is unusually close and you should run it honestly. Model an organisation of 400 knowledge workers recording about eleven meetings each per month, which is the 4,400 figure above. Buying at a documented $19 per user per month billed annually is $7,600. Building is $3,652 of variable cost plus about $500 of infrastructure, plus roughly eleven engineer-weeks amortised over 24 months at $1,375 a month, plus three engineer-days a month of maintenance at $1,800 — about $7,327. The crossover on those inputs is around 370 seats, and at 400 seats the two options are within 4% of each other, which is well inside the error bars of every assumption in the box above.
That closeness is the answer, not a failure of the analysis. At under about 370 seats, buy — the vendors are efficient and your engineering time is worth more elsewhere. Build when one of three things is true: you need writes into systems no vendor integrates with, you need the recordings and transcripts to stay inside your own infrastructure for regulatory reasons, or the meeting corpus is a strategic asset you intend to build products on rather than a convenience. The third reason is the one that actually justifies most real builds, and it is not about cost at all.
Two cost levers matter and neither is the model. The first is recording hygiene: at 45 cents of capture per meeting, recording every recurring standup that nobody ever queries is pure waste, and a simple rule — record meetings with three or more attendees, or an agenda, or an external participant — routinely removes a third of volume. The second is the diarisation decision described earlier, which is a doubling. Everything else is rounding. For the model-side machinery in general, see LLM routing, caching and cost per request.
| Line item | Model / rate | Units | Cost | Share | Note |
|---|---|---|---|---|---|
| Meeting capture | Recall.ai-class · $0.50 per recording hour | 45 min | $0.37500 | 45.0% | Charged whether the meeting is ever queried again or not |
| Recording storage | $0.05 per hour per 30-day period | 0.75 hr, first 7 days free | $0.03750 | 4.5% | Retention policy is a cost decision as well as a legal one |
| ASR + diarisation | Deepgram Nova-3 batch $0.0043/min + $0.002/min | 45 min | $0.28350 | 34.0% | Streaming would be roughly 80% more per minute for no batch benefit |
| Speaker resolution | Haiku 4.5 · $1 / $5 per 1M | 6.0k in / 300 out | $0.00750 | 0.9% | Bounded to the calendar attendee list. Best value in the pipeline |
| Extraction (3 chunks) | Sonnet 5 · $2 / $10 per 1M | 5.0k in / 700 out each | $0.05100 | 6.1% | Two-minute overlap so decisions are not lost at boundaries |
| Consolidation pass | Sonnet 5 | 6.0k in / 900 out | $0.02100 | 2.5% | Dedupes on verbatim span, never on generated title |
| Verification | Haiku 4.5 | 12.0k in / 600 out | $0.01500 | 1.8% | Span, owner and date checks are deterministic and cost $0 |
| Summary + attendee recap | Sonnet 5 | 12.0k in / 800 out | $0.03200 | 3.8% | The commoditised part of the product, priced accordingly |
| Tracker field mapping | Haiku 4.5 | 8.0k in / 400 out | $0.01000 | 1.2% | Maps typed items onto this workspace's custom fields |
| Embeddings | text-embedding-3-small · $0.02 per 1M | 10.0k tokens | $0.00020 | 0.02% | A full year of 53,000 meetings embeds for about $10.60 |
| Total per meeting | — | — | $0.83270 | 100% | $0.6965 of it spent before the first language model call |
- Meeting capture bot$0.375 · 45%
- Transcription + diarisation$0.284 · 34%
- LLM extraction + consolidation$0.072 · 9%
- LLM summary, verify, map, speakers$0.065 · 8%
- Storage + embeddings$0.038 · 5%
What breaks in production, and how would you know?
Twelve things, and the three most damaging all produce clean, well-formed, fully cited output. A speaker swap that assigns an action to the wrong person, a hypothetical promoted to a task, and a superseded decision returned as current all look perfect on every dashboard. No error rate moves and no exception is thrown.
Read the table by its second column. The row that determines whether this product survives is the speaker swap, because it is the failure with a human victim: someone gets a task in their queue for work they did not agree to do, in a meeting they may not remember clearly, attributed with apparent confidence. Two or three of those and the tool is discussed in a retro, and tools discussed in retros do not get renewed. This is why the write path drives the microphone decision and why overlapping turns should cost an item its ability to auto-write.
The consent row is the one that is not an engineering failure at all. Recording rules differ by jurisdiction and several require all parties to consent, so consent state has to be captured per participant, stored, and enforced before audio is retained — not as a checkbox in an admin panel but as a precondition on the storage write. A pipeline that records first and checks later has already created the artefact that is the problem.
Four signals catch most of the rest. Span-not-found rate is the cleanest hallucination metric available anywhere in this design and it requires no labelling: count how often the model produces a span that is not a substring of any transcript turn. Item rejection rate in the human confirm queue, broken out by reason code, tells you which check is doing work and which is miscalibrated. Duplicate-key collision rate on reprocess should be exactly equal to the reprocess volume — if it is lower, your idempotency is broken. And overlap share per meeting is a leading indicator: a meeting with 30% overlapping speech should produce mostly offered rather than written items, automatically.
The last row of the table is the standard one and it holds here. If you cannot answer, six weeks later, why a specific task was created and assigned to a specific person — which meeting, which turn, which quote, which confidence score, which check passed last — then you cannot resolve a dispute, and disputes are the moments that decide whether an organisation keeps this system. One journal table, one query.
| Failure mode | What the user sees | Where to fix it | Detection signal | Cost of getting it wrong |
|---|---|---|---|---|
| Speaker swap during overlapping speech | A task assigned to a colleague who never agreed to it | Capture: per-participant channels. Runtime: overlap flag blocks auto-write | Overlap share per meeting; owner-corrections rate in the confirm queue | The failure that gets the product uninstalled. Two or three is enough |
| Hypothetical promoted to an action item | A task from someone should probably look at this | Extraction: commitment regex plus schema constraint, not a prompt instruction | Rejection reason unowned_hypothetical as a share of extracted items | Tracker noise. At scale this is indistinguishable from the product being broken |
| Model fabricates a span | A confident quote nobody said | Verification: exact substring match against the claimed turn, drop on failure | Span-not-found rate — free, unlabelled, continuously available | Caught before the user sees it, if and only if you built the check |
| Superseded decision returned as current | A plan built on a decision reversed three months ago | Index: decision entities with a topic key and a supersedes edge | Share of decision answers with no supersession check; user corrections | Systemic. Every future query on that topic is wrong until the graph is fixed |
| Duplicate tasks on reprocess | Three copies of every action item after a backfill | Write path: idempotency key from meeting id plus span hash, never title hash | Key-collision rate on reprocess should equal reprocess volume exactly | Adoption collapse. A noisy tracker is worse than no tracker |
| Relative date resolved by the model | Due next Friday resolved to the wrong Friday, or the wrong year | Verification: deterministic parser against meeting date and timezone | Date-unresolvable rate; distribution of due dates more than 90 days out | Missed commitments that look like the person forgot rather than the tool |
| Consent not captured for a jurisdiction | Nothing. Until a legal request arrives | Capture: consent state as a precondition on the storage write, per participant | Recordings stored with incomplete consent records — should be exactly zero | Legal exposure. The one row here that engineering cannot remediate afterwards |
| Permissionless index | Someone retrieves a compensation discussion they were not in | Retrieval: ACL predicate inside the query, in the same store, never post-filtered | Access-audit assertions in CI; retrieval attempts blocked by ACL, by user | Serious internal breach. Treat as sev-1 even when nobody complains |
| Decision lost at a chunk boundary | The one thing the meeting existed to settle is missing | Extraction: overlapping chunks plus consolidation dedup on verbatim span | Recall against a small hand-labelled set of meetings, refreshed monthly | Silent. Users conclude the tool misses important things and stop reading it |
| Domain vocabulary mis-transcribed | Customer and product names garbled throughout | ASR: keyterm prompting with your account and product lists, refreshed weekly | Rate of extracted entities that fail to resolve against CRM and directory | Erodes trust gradually; the most common reason people call output low quality |
| Recording meetings nobody queries | Nothing visible. A bill that grows faster than usage | Capture policy: three-plus attendees, an agenda, or an external participant | Share of recorded meetings never retrieved or extracted from within 30 days | Roughly 45 cents per pointless meeting — about a third of volume in practice |
| No run journal | Nobody can explain why a task was created six weeks ago | Journal every stage, model call, score, token count and dollar in Postgres | Can you answer why did this task appear from one SELECT — yes or no | You lose every dispute, which is how organisational trust in the tool ends |
- Span-not-found rateThe cleanest hallucination metric in the design. A model-produced span that is not a substring of any turn is unambiguously fabricated. No labelling, no judge, no eval set.
- Confirm-queue rejection rate by reason codeTells you which verification check is doing real work and which is miscalibrated. If unowned_hypothetical is 40% of rejections, your extraction prompt is too eager.
- Owner-correction rate in the confirm queueThe direct measure of speaker resolution quality in production, and the number that decides whether you need per-participant channels.
- Overlap share per meetingA leading indicator. Meetings above roughly 25% overlapping speech should automatically produce offered rather than written items.
- Idempotency collision rate on reprocessOn a backfill this should equal reprocess volume exactly. Anything lower means you are about to create duplicates at scale.
- Recorded meetings never queried within 30 daysAt 45 cents of capture each, this is the largest pure-waste line in the budget and typically a third of volume.
- Action-item completion rate versus the pre-tool baselineThe only metric that measures whether the product works rather than whether the pipeline works. Requires a baseline nobody captured, which is why it is never built.
What does it take to build, and what should you skip in v1?
About eleven engineer-weeks for a two-person team to a production-grade v1, plus roughly three engineer-days a month of maintenance. Skip real-time in-meeting features, video understanding, custom voice enrolment and multilingual until the core write path is trusted, because each is a month spent before you know whether anyone acts on the output.
Week one is capture and consent, which is legal work with a small amount of code attached: bot join or host SDK, per-participant consent state, retention policy, and the storage precondition that enforces it. Week two is the transcript pipeline and the journal. Weeks three and four are speaker resolution and the directory integration, which sounds small and is not — join logs, calendar edge cases, guests without directory entries, people who dial in from a phone.
Weeks five and six are typed extraction and the verification gate, which is where the product actually lives. Week seven is the MCP write surface and idempotency. Weeks eight and nine are the index, the ACL model and the entity graph with supersession. Weeks ten and eleven are the confirm queue interface, dashboards, evals against a hand-labelled set of meetings, and the rollout — which for this product means picking one team, running it for three weeks, and reading every rejected item with them.
Skip real-time in-meeting features first. Live agenda tracking and in-call nudges are a different product with a different latency budget and roughly 80% higher per-minute transcription cost, and they are the single most requested demo feature. Skip video understanding — slide contents, whiteboard capture, screen share OCR — until text extraction is trusted, because it multiplies both cost and failure surface for content that is usually also in a document you already index. Skip custom voice enrolment: the calendar attendee list gets you most of the way for under a cent, and enrolment is a consent conversation with every employee.
What you must not skip: verbatim span verification, closed-set owner resolution, deterministic date parsing, idempotency on the span hash, the ACL predicate, and the confirm queue. Those six are about three of the eleven weeks and they are the difference between a system a company adopts and a system a company quietly stops opening. If you want this architected and built rather than described, that is what AI product development is for, and the broader stack these components sit in is in the AI product architecture guide. The document-side analogue, with the same verification discipline applied to files rather than audio, is in the document processing pipeline build.
- Week 1Capture, consent, retention
Bot join or host SDK, per-participant consent state stored as a precondition on the storage write, retention policy. Mostly legal design with code attached, and impossible to retrofit.
- Week 2Transcript pipeline and journal
Batch ASR with diarisation, transcript store with turn-level segments and spans, run and stage tables, cost attribution with a rate-card id. No demo value, total dependency.
- Weeks 3‑4Speaker resolution
Calendar and directory integration, join-log matching, guests without directory entries, phone dial-ins, overlap flagging. Sounds small; it is the accuracy ceiling of the product.
- Weeks 5‑6Typed extraction and verification
Overlapping chunks, consolidation with span dedup, the item schema, the commitment check, deterministic date resolution, closed-set owner resolution. This is the product.
- Week 7MCP write surface
Eight tools, idempotency on meeting id plus span hash, attribution embedded in every created object, one-click reversal. Deliberately no closes, no reassignments, no stage changes.
- Weeks 8‑9Index, ACLs and entity graph
Turn-aligned chunking, hybrid search, the ACL predicate inside the query, decision entities with topic keys and supersedes edges. The half that compounds in value.
- Weeks 10‑11Confirm queue, evals, rollout
The one-click confirmation interface, dashboards, a hand-labelled eval set of twenty real meetings, and three weeks with one team where you read every rejected item together.
- Seats
- 400
- Rate
- $19 per user per month, annual
- Monthly cost
- $7,600
- Writes into your tracker and CRM
- Whatever the vendor integrates with
- Recordings and transcripts
- In the vendor's infrastructure
- Meeting corpus as a product surface
- Not available
- Variable cost
- 4,400 x $0.833 = $3,652
- Infrastructure and observability
- $500
- Amortised build (11 weeks / 24 months)
- $1,375
- Maintenance (3 engineer-days/month)
- $1,800
- Monthly cost
- $7,327
- Cost per meeting
- $1.67 all-in
Building AI meeting intelligence: common questions
→How much does AI meeting intelligence cost per meeting?
On the model in this post, about 83 cents for a 45-minute meeting with five participants: 37.5 cents of capture at a modelled $0.50 per recording hour, 28.4 cents of batch transcription with diarisation at $0.0043 plus $0.002 per minute, and roughly 13 cents across every language model call in the pipeline. Storage adds about four cents. The striking part is the ratio — 79% of the cost is spent before a language model sees a single word, which means prompt-level optimisation moves almost nothing and recording policy moves a great deal. These are modelled figures from published list rates, not measurements of a system I have operated.
→Should I build meeting intelligence or buy an existing tool?
Buy it below roughly 370 seats. Modelled against Fireflies Business at a documented $19 per user per month billed annually, a 400-seat organisation recording about eleven meetings per person per month pays $7,600 to buy and about $7,327 to build once you include capture, transcription, models, infrastructure, eleven engineer-weeks amortised over 24 months and three engineer-days a month of maintenance. Those two numbers are within 4%, which means cost is not the deciding factor. Build when you need recordings to stay in your own infrastructure, when you need writes into systems no vendor integrates with, or when the meeting corpus is something you intend to build products on.
→How accurate is speaker diarisation, and does it matter?
Published benchmark write-ups put pyannote.audio 3.1 at roughly 11% diarisation error rate on the AMI meeting corpus and commercial APIs in the 8-14% band on comparable audio, rising to a reported 25-40% on overlapping speech. It matters enormously, because a wrong speaker label becomes a wrong task owner and no text-level check can detect it — the quote is real, the task is real, the person is wrong. If a human confirms every write, that error rate is survivable. If you write owners automatically, you need per-participant audio channels, which roughly doubles the cost per meeting.
→How do you stop the system creating tasks nobody agreed to?
With a schema constraint rather than a prompt instruction. An action item must carry a verbatim transcript span, and that span must contain a first-person or directly-addressed commitment — I will, we will, let me. A phrase like someone should probably ask Legal fails that check deterministically and goes to a human confirmation queue with the reason shown rather than into the tracker. Owners are resolved against a closed set of attendees and directory entries, and dates are resolved by a parser against the meeting date rather than by the model. Three deterministic checks, all free, catching the failure that otherwise buries a task tracker in noise.
→Can the agent write directly into Jira, Linear or Salesforce?
Yes, and it should — that is where the value is — but with a deliberately anaemic tool surface. Create a task, write a decision, log a call note, comment on objects the pipeline itself created, and a handful of reads for deduplication and field schemas. Every write is idempotent on a key derived from the meeting id and a hash of the verbatim span, so reprocessing a meeting after a model upgrade creates zero duplicates. Closing tasks, reassigning existing tasks and changing CRM opportunity stages should not be exposed at all: those are human judgements, and a wrong one is discovered by a finance team rather than by your monitoring.
→How do you search across months of meetings without leaking anything?
Enforce access control as a predicate inside the retrieval query, in the same store as the data, never as a filter applied to results after they come back. A transcript chunk is visible to the people who were in that meeting plus explicit grants. This corpus contains compensation conversations, performance discussions and legal strategy, so post-filtering is not an acceptable design — it leaks through result counts and timing before it leaks through a bug. Alongside permissions, give decisions a topic key and a supersedes edge so that a question about a past decision returns the current one with its date and what it replaced.
→Should transcription run live during the meeting or after it?
After, unless you are deliberately building a different product. Batch transcription at the modelled rates is roughly 80% cheaper per minute than streaming, and the four-minute post-meeting delay is useful rather than costly — it is the window in which consolidation, deduplication against existing tickets and entity lookups actually happen. Live in-meeting features such as agenda tracking or real-time nudges are genuinely valuable to some teams, but they are a separate latency budget, a separate cost model and a separate set of failure modes, and they should be a deliberate phase two rather than an accidental architecture choice made because streaming demos better.