Every meeting that s1ft records gets a folder, and three plain JSON files in it carry the data: meeting.json, transcript.json and telemetry.json. This page lists every field of each file, with its type and its schema version, so you or an AI agent can read them without guessing. For the agent setup, see Connect AI agents.
Before you start
- The files sit in the meeting folder. To find the folder, see Finding your recordings.
meeting.jsonis the last data file s1ft writes for a meeting. When it exists, the transcript is ready.- Every file is written in one step, so a reader sees the old file or the complete new one, never half a file.
- Times in
meeting.jsonare ISO 8601 date-times, for example2026-07-16T10:00:00Z. Times intranscript.jsonandtelemetry.jsonare seconds. - The files hold measurements and text. They hold no scores, no labels and no summary. A figure such as Talk ratio or Filler rate is not stored in any file. An agent can work it out from the segments in
transcript.json.
Schema versions and the add-only rule
Each file starts with a schemaVersion number. Read it first, then read the fields.
| File | Current schemaVersion |
|---|---|
meeting.json | 1 |
transcript.json | 1 |
telemetry.json | 2 |
A new schema version only adds fields. It never removes a field and never renames one. Code that reads version 1 keeps working when version 2 appears, as long as it ignores fields it does not know. The JSON Schemas below leave extra fields allowed for the same reason.
Meetings from before meeting.json
The command line reads meetings through meeting.json. A folder without that file is not a meeting for the CLI. Meetings recorded before meeting.json existed (before S1ft 1.3) are therefore not visible to the s1ft command, and s1ft does not change, convert or remove the files of those meetings. If a list comes back empty, see Where the agent looks for meetings.
meeting.json
meeting.json says which meeting the folder holds: its id, its name, when it ran and which calendar events overlapped it. Current version: 1.
Fields
| Field | Type | Meaning |
|---|---|---|
schemaVersion | integer | The version of this file. Always 1 today. |
id | string (UUID) | The id of the meeting. The s1ft command takes this id. |
name | string | The meeting name as s1ft shows it. If you rename a meeting, s1ft writes the file again with the new name. |
platform | string | The bundle identifier of the app that was recorded, for example us.zoom.xos. A manual recording gives manual. |
start | string (date-time) | When the recording started. |
end | string (date-time) | When the recording ended. |
calendarEvents | array of event | The calendar events that overlapped the recording. An empty array when no calendar account is turned on or nothing matched. |
calendarEvents items
Each entry in calendarEvents has these fields.
| Field | Type | Meaning |
|---|---|---|
title | string | The title of the event. |
start | string (date-time) | When the event starts. |
end | string (date-time) | When the event ends. |
isAllDay | boolean | true for an all-day event. |
attendees | array of attendee | The people on the event. |
usedForName | boolean | true only for the event that gave the meeting its name. After you rename a meeting, or when no event gave the name, every event has false. |
attendees items
| Field | Type | Meaning |
|---|---|---|
name | string or null | The attendee name. null when the calendar has none. |
email | string or null | The attendee email. null when the calendar has none, for example for a room. |
Always check for null before you use name or email.
Example
{
"calendarEvents": [
{
"attendees": [
{ "email": "[email protected]", "name": "Ana" },
{ "email": null, "name": "Room 4" }
],
"end": "2026-07-16T10:30:00Z",
"isAllDay": false,
"start": "2026-07-16T10:00:00Z",
"title": "Design Review",
"usedForName": true
}
],
"end": "2026-07-16T10:30:00Z",
"id": "AAAAAAAA-0170-4000-8000-000000000001",
"name": "Design Review",
"platform": "us.zoom.xos",
"schemaVersion": 1,
"start": "2026-07-16T10:00:00Z"
}JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "meeting.json (schemaVersion 1)",
"type": "object",
"required": ["schemaVersion", "id", "name", "platform", "start", "end", "calendarEvents"],
"properties": {
"schemaVersion": { "const": 1 },
"id": { "type": "string", "format": "uuid" },
"name": { "type": "string" },
"platform": { "type": "string" },
"start": { "type": "string", "format": "date-time" },
"end": { "type": "string", "format": "date-time" },
"calendarEvents": {
"type": "array",
"items": {
"type": "object",
"required": ["title", "start", "end", "isAllDay", "attendees", "usedForName"],
"properties": {
"title": { "type": "string" },
"start": { "type": "string", "format": "date-time" },
"end": { "type": "string", "format": "date-time" },
"isAllDay": { "type": "boolean" },
"attendees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] }
}
}
},
"usedForName": { "type": "boolean" }
}
}
}
}
}transcript.json
transcript.json holds the words of the meeting, split by speaker. Current version: 1.
Fields
| Field | Type | Meaning |
|---|---|---|
schemaVersion | integer | The version of this file. Always 1 today. |
segments | array of segment | The transcript, in order. |
Each segment has exactly four fields.
| Field | Type | Meaning |
|---|---|---|
speaker | string | Who spoke. Me is you, from your microphone. Other speakers are Participant 1, Participant 2 and so on, numbered by when they first speak. When s1ft finds only one other speaker, the label is Participant. |
start | number | Start of the segment, in seconds. |
end | number | End of the segment, in seconds. |
text | string | The words of the segment. |
Older meetings keep a transcript.md file instead. s1ft does not convert it.
Example
{
"schemaVersion": 1,
"segments": [
{ "end": 5.5, "speaker": "Me", "start": 0, "text": "Hello everyone" },
{ "end": 9, "speaker": "Participant 1", "start": 5.5, "text": "Hi" }
]
}JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "transcript.json (schemaVersion 1)",
"type": "object",
"required": ["schemaVersion", "segments"],
"properties": {
"schemaVersion": { "const": 1 },
"segments": {
"type": "array",
"items": {
"type": "object",
"required": ["speaker", "start", "end", "text"],
"properties": {
"speaker": { "type": "string" },
"start": { "type": "number" },
"end": { "type": "number" },
"text": { "type": "string" }
}
}
}
}
}telemetry.json
telemetry.json holds raw measurements of your own microphone, frame by frame. It has no averages, no scores and no data about other speakers. Current version: 2.
Version 2 holds one span for each segment that transcript.json labels Me. Inside a span, the frames come one after the other: frame i starts at start + i * frameMs / 1000 seconds, where frameMs is the value in method.
Fields
| Field | Type | Meaning |
|---|---|---|
schemaVersion | integer | The version of this file. 2 for the current format. |
source | string | Where the audio came from. Always microphone. |
method | object | How the frames were measured. See below. |
sampleRateHz | integer | The sample rate of the microphone audio, in Hz. |
spans | array of span | One entry for each Me segment. |
The method object:
| Field | Type | Meaning |
|---|---|---|
frameMs | integer | The length of one frame, in milliseconds. |
windowSamples | integer | The number of audio samples that each measurement looks at. |
pitchAlgorithm | string | The pitch method. autocorrelation. |
pitchRangeHz | array of two integers | The lowest and the highest pitch the method looks for, in Hz. |
Each span has four fields, and the three arrays have one value per frame.
| Field | Type | Meaning |
|---|---|---|
start | number | Where the span starts, in seconds. |
pitchHz | array of number or null | The pitch of each frame, in Hz. null for a frame with no pitch, for example a silent frame. |
rmsDbfs | array of number | The average loudness of each frame, in dBFS. |
peakDbfs | array of number | The peak level of each frame, in dBFS. |
Example
{
"method": {
"frameMs": 100,
"pitchAlgorithm": "autocorrelation",
"pitchRangeHz": [50, 1000],
"windowSamples": 2048
},
"sampleRateHz": 48000,
"schemaVersion": 2,
"source": "microphone",
"spans": [
{
"peakDbfs": [-6, -6],
"pitchHz": [220, null],
"rmsDbfs": [-9, -60],
"start": 1
}
]
}JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "telemetry.json (schemaVersion 2)",
"type": "object",
"required": ["schemaVersion", "source", "method", "sampleRateHz", "spans"],
"properties": {
"schemaVersion": { "const": 2 },
"source": { "type": "string" },
"method": {
"type": "object",
"required": ["frameMs", "windowSamples", "pitchAlgorithm", "pitchRangeHz"],
"properties": {
"frameMs": { "type": "integer" },
"windowSamples": { "type": "integer" },
"pitchAlgorithm": { "type": "string" },
"pitchRangeHz": { "type": "array", "items": { "type": "integer" } }
}
},
"sampleRateHz": { "type": "integer" },
"spans": {
"type": "array",
"items": {
"type": "object",
"required": ["start", "pitchHz", "rmsDbfs", "peakDbfs"],
"properties": {
"start": { "type": "number" },
"pitchHz": { "type": "array", "items": { "type": ["number", "null"] } },
"rmsDbfs": { "type": "array", "items": { "type": "number" } },
"peakDbfs": { "type": "array", "items": { "type": "number" } }
}
}
}
}
}Meetings recorded before version 2 hold a version 1 telemetry.json with a different shape. The s1ft command reads version 2 only.
Numbers from the frames: s1ft voice stats
Raw frames are long. The command s1ft voice stats <id> turns the frames of telemetry.json into a short set of numbers for each span. It reads the file and saves nothing. Add --json to get JSON on stdout.
--silence-threshold-dbfs, --min-silence-ms and --percentiles change the thresholds. The defaults are -50, 300 and 10,50,90.
Output fields
| Field | Type | Meaning |
|---|---|---|
frameMs | integer | The frame length, copied from method.frameMs. |
options | object | The thresholds in use: silenceThresholdDbfs (number), minSilenceMs (integer) and percentiles (array of integers). |
spans | array of span stats | One entry for each span of telemetry.json, in the same order. |
Each entry in spans has these fields.
| Field | Type | Meaning |
|---|---|---|
start | number | Where the span starts, in seconds. |
durationSec | number | The length of the span, in seconds. |
frameCount | integer | The number of frames. |
voicedFrameCount | integer | The number of frames that have a pitch. |
pitchMinHz | number | The lowest pitch. Absent when no frame has a pitch. |
pitchMaxHz | number | The highest pitch. Absent when no frame has a pitch. |
pitchRangeHz | number | Highest minus lowest pitch. Absent when no frame has a pitch. |
pitchPercentilesHz | object | Pitch values by percentile, with keys such as p10, p50 and p90. Empty when no frame has a pitch. |
rmsMeanDbfs | number | The mean of rmsDbfs. Absent when the span has no frames. |
peakMaxDbfs | number | The highest value of peakDbfs. Absent when the span has no frames. |
silenceCount | integer | The number of silences. A silence is a run of frames below the silence threshold that lasts at least minSilenceMs. |
silenceTotalSec | number | The total length of those silences, in seconds. |
cli.json
The s1ft command keeps one small settings file, ~/Library/Application Support/s1ft/cli.json. It has one key.
| Field | Type | Meaning |
|---|---|---|
meetingsDir | string | The meetings folder that the command uses. s1ft config dir <path> sets it. The folder must exist. |
--dir beats this key, and this key beats the output folder set in the s1ft app. See Where the agent looks for meetings for the full order.
What’s next
- Connect AI agents — Install the s1ft skills and run them after each meeting.
- Finding your recordings — Where each meeting folder lives and what else is in it.
- Review your delivery with an AI agent — Use your voice numbers for a review of how you speak.