---
title: File shape
description: "One markdown file per meeting: date-first name, YAML frontmatter, merged speaker paragraphs."
---

# File shape

One markdown file per meeting. The frontmatter is the schema — it costs nothing
now and it is exactly what you would load into a database later.

## Naming

```text
  transcripts/internal/2026/08/2026-08-26-weekly-sync-a1b2c3d4.md
                       ^^^^ ^^ ^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^
                       year mo  meeting     topic      first 8 chars of a
                                start date  slug       hash of the UUID
```

- Date first, so chronological sort is free and an agent can window by prefix.
- Topic slug, so a human finds it without opening it.
- **The hash suffix is non-negotiable.** Recurring meetings share one meeting
  ID. Two occurrences of the same meeting on one day — rescheduled, restarted —
  would collide. Hash rather than raw UUID, because raw UUIDs are base64 and
  contain `/` and `+`.
- Client calls live in a separate tree. See
  [Consent](/be-pipeline-meeting-transcripts/consent).

## Frontmatter

```yaml
---
meeting_uuid: "<meeting-uuid>"        # primary key, quoted: base64 has / and +
meeting_id: 00000000000               # stable across a recurring series
topic: "Weekly Sync"
start_time: 2026-08-26T14:00:00Z
end_time: 2026-08-26T14:47:12Z
duration_min: 47
host_email: sample.person@example.com
participants: [sample.person@example.com, second.person@example.com]
source: zoom_ai_companion_transcript
confidentiality: internal             # internal | client
client: null                          # or the account slug, e.g. acme-corp
fetched_at: 2026-08-27T06:00:11Z
---
```

Decide these field names once. Renaming a field across 3,000 files later is
annoying; deciding now is free.

`source` matters: an agent should know whether it is reading an AI Companion
transcript or a cloud recording one, because their speaker attribution differs.

**Participants [SPEC].** They are not in the transcript response — that is a
second call and a second scope to get approved. Derive the list from the
distinct speaker labels in the VTT instead. You lose anyone who never spoke. For
a five-person team that rarely matters. Add the call only when someone asks.

## VTT to markdown

Raw VTT is hundreds of three-second cues. It costs 3–5× the tokens and reads
like confetti.

```text
  BEFORE (raw VTT)                    AFTER (markdown)
  1                                   ## 00:00
  00:00:04.120 --> 00:00:06.880
  Sample Person: First half of         **Sample Person** — First half of a
                                       sentence, second half of the same
  2                                    sentence, all one paragraph.
  00:00:06.880 --> 00:00:09.410
  Sample Person: second half of        **Second Person** — A reply.
```

1. **Merge consecutive cues from the same speaker into one paragraph.** Biggest
   single win. Everything else is polish.
2. Drop cue indexes and end timestamps. Keep one coarse `## MM:SS` heading every
   five minutes so a human can scrub back.
3. **Keep the raw `.vtt` next to the `.md`.** Kilobytes, and it is the only way
   to fix a bad transform without re-hitting the API.

## Speaker attribution — be honest

Zoom labels by account display name, so it is exact when everyone is signed in
on their own device. "Unknown Speaker" appears when Zoom cannot attribute a
segment. **A shared conference room collapses everyone in it into one label**,
and nothing downstream fixes that. Route 3's language coverage is
**[UNVERIFIED]**.
