Skip to content

Meetings

Meetings let a project or group capture a meeting transcript and store it server-side. The stored copy is DLP-redacted under the strictest of the caller’s org policies, participants can be resolved to org members or personas, transcripts can be extracted into action items and decisions, and those action items can be promoted to tickets. Transcripts age out under an org-level retention window.

The meetings feature is available on every tier. Group-scoped meetings additionally require the project_groups feature (Team+), and promoting an action item to a ticket requires agent_tickets (every plan).

Every meeting belongs to exactly one of:

  • a project (project_id), or
  • a group (group_id).

That is the meeting’s scope. Its owning org is taken from the scope; a personal project has none.

Access follows the scope’s normal standing:

Action Project meeting Group meeting
List / get project read (or a service key with meetings:read + project allowlist) group reader (user key)
Upload project edit (or a service key with meetings:write + project allowlist) group editor or admin
Delete uploader or project admin (a service key’s backing user must be one of these) uploader or group admin
Override participant project editor group editor or admin

Group meetings always require a user key. There are no org-wide meetings.

POST uploads a meeting with a title (1–200 chars), content (the transcript text), an optional meeting_at / duration_minutes, and a source (manual_paste or file_upload). Set extract: true to run action-item extraction in the same request. WebVTT and SRT content has its cue timing stripped to Speaker: text lines whichever source it arrives with, so a caption file pasted as text is stored the same as one uploaded as a file (the signature check is exact; plain text is never changed). In the dashboard, a pasted transcript you don’t name gets its first line as the title when that reads like a heading, otherwise Meeting on <date>; you can edit it before saving.

The request body is streamed with a hard cap of 1 MiB; the normalised content is capped at 512 KiB and 10 000 lines. Over the cap the upload is refused with 413 meeting_too_large.

Before storage the title and content are normalised (newlines collapsed in the title, CRLF → LF, NULs and lone surrogates stripped, trailing whitespace trimmed per line) and scanned under the strictest union of every org the caller is an active member of, plus the scope org. Secrets are always scanned. A block-mode finding refuses the upload with 422 dlp_blocked (counts by finding type). In every other mode — redact, warn, or no policy at all — secrets are redacted before storage and counted in dlp_findings. The stored title and content are the only copy kept — the raw text is never persisted.

Re-uploading identical content still stores a new meeting, with warnings: ["duplicate_of: meet_…"] pointing at the earlier one.

Each named participant in the transcript is resolved over the meeting’s scope (its org), in order:

  1. an exact email matching an active org member,
  2. an exact persona name (project personas first, then the org’s personas the caller can see),
  3. a display name matching exactly one active member,
  4. the exact email of an active external contact of the org,
  5. a name or alias matching exactly one active external contact,

otherwise the participant stays unresolved. Extraction never creates a contact: unresolved names wait in the org’s People to confirm queue until a person creates the contact or adds the name as an alias. A persona that represents a member fills both persona_name and user_id. Personal meetings have no org, so only a project persona resolves; otherwise participants stay unresolved until overridden.

A project or group editor can override any participant manually with exactly one of user_id, persona_name or contact_id (an active contact of the meeting’s org), which sets the resolution method to manual and records who made the override.

Extraction turns a stored transcript into a list of participants and items (each an action or decision) with one LLM call. Run it on upload with extract: true, or later with POST /api/v1/meetings/{meeting_id}/extract.

The model is given the meeting date (its meeting_at, else the day it was stored) so relative due dates such as “by Friday” resolve to the right day, and is asked for names, owners, due dates and 1-based line citations, but everything it returns is validated against the stored text before it is persisted: participants are resolved server-side (same order as participant resolution), and each item must cite a valid line range (at most 40 lines apart). An item whose owner matches no participant (or whose owner isn’t named in the cited lines) is kept with no owner: owner_participant_id is null and owner_hint carries the owner text the model gave (DLP-redacted, never used to assign anyone). The dashboard shows it as Owner unknown and asks you to choose an assignee when you create a ticket from it. dropped_items counts only entries that fail validation (bad kind, line range, date or field types).

Extraction uses the same LLM key resolution and monthly token budget as the session helper. With no key configured it returns 409 llm_key_required; an exhausted org budget returns 429 helper_budget_exceeded. An organization can run 4 extractions at a time across all servers by default (a personal project counts against its owner); beyond that extraction returns 429 org_concurrency_limit with Retry-After (see Tenancy and Isolation). An upload with extract: true is still stored and its response carries the warning extraction_skipped: org_concurrency_limit. AI usage is recorded as meeting_extract, separately from helper chats, and counts against the same org budget. The provider call has a 240-second deadline (reasoning models can need over two minutes on an hour-long transcript); past it the meeting is marked failed with timeout. A provider failure or an unparseable response marks the meeting extraction_status: "failed" with a classified extraction_error and leaves existing items untouched.

Re-running replaces pending items and every non-manual participant; promoted / dismissed / linked items and manually overridden participants are kept, so a re-extract never undoes a promotion, a dismissal or a link.

Section titled “Existing tickets: suggest, never auto-link”

Extraction also looks for tickets that already exist. Before the model call the server shortlists up to 30 tickets that match the transcript text (a deterministic BM25 ranking over the newest 500 tickets by last update) and passes their id, status and title to the model in a delimited block; ticket titles are treated as untrusted text exactly like transcript lines. Candidates come from the meeting’s own project, or for a group meeting from the group’s member projects you can read. Cancelled tickets are never candidates; done or closed tickets are, if they were resolved in the last 90 days, so “this was already fixed” shows up.

The model may name one shortlisted ticket per item. Any other id is dropped. When the model names none, the server compares the item’s own text with ticket titles and suggests the best match only when it is clearly ahead (a weighted word-overlap score of at least 0.5, at least two shared words, and a clear lead over the next ticket). The item then carries suggested_ticket (id, project_id, title, status), suggestion_source (model or text) and suggestion_score. Visibility is checked on every read: if you can no longer read the ticket from this meeting’s scope, the suggestion is simply absent.

Nothing is linked automatically. In the dashboard a pending item with a suggestion shows Looks like tk_… · title · status with three choices:

  • Link ties the item to that ticket: the item becomes linked, the ticket gets a comment (Raised again in meeting "<title>" on <date>, lines L<a>–<b>: <item text>, DLP-redacted with your org’s strictest policy) and a timeline event, and any reminders on the item also show on the ticket.
  • Not this one hides the suggestion; the item stays pending.
  • Create new is the usual promote flow below.

Any pending item can also be linked to a ticket you pick (Link to ticket…), with or without a suggestion. The ticket must be in the meeting’s project (for a group meeting, a member project you can read) and you must be able to comment on it; any other ticket is refused with the same 422 ticket_not_linkable, which never says whether the ticket exists. Unlink puts a linked item back to pending; the comment stays on the ticket (comments are append-only) and the ticket’s timeline records the unlink.

A pending action item (never a decision) can be promoted to a ticket. First build a confirmable draft with POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/promote/preview, then confirm with POST .../promote (body: title ≤ 200, description, priority, kind, optional assigned_to / assigned_user_id / due_date, and for a group meeting target_project_id). The preview also returns jira_publish_available (true only when the plan includes Jira, you can edit the target project, the org has an enabled Jira connection and the project is mapped; computed from the database, never by calling Atlassian). For a group meeting it is judged for the project passed as ?target_project_id= and is false without one. The draft title is the item’s first sentence; the description carries the item text plus a From meeting … provenance header and the quoted source lines.

The target project is the meeting’s own project for a project meeting, or an editable project the caller chooses from the group for a group meeting (omitting it returns 422 target_project_required). Confirming creates the ticket, flips the item to promoted with promoted_ticket_id, and — with publish_to_jira: true — publishes it to Jira. A concurrent promote is refused with 409 item_not_pending.

Dismiss a pending item with POST .../dismiss (a second dismiss returns 409 item_not_pending) and bring it back with POST .../restore (409 item_not_dismissed if it was not dismissed).

Extraction never creates reminders, but an action item or decision with a due date carries a suggested_reminder (title, due_date, due_at at 09:00 UTC on that date, the cited lines, and owner_user_id when the item’s owner resolved to a person). POST /api/v1/projects/{project_id}/meetings/{meeting_id}/items/{item_id}/reminder (or /api/v1/groups/{group_id}/… for a group meeting) creates it, pre-filled from the suggestion; pass title, due_at, timezone or owner to change it. The reminder points back at the item and its lines. When the item is later promoted, its reminders are linked to the new ticket and also show on the ticket. GET …/meetings/{meeting_id}/reminders lists the reminders on a meeting and its items that you can see (your own, or ones you set for someone else). Group meetings follow the group’s visibility rules.

sfs meeting is a thin client of the REST routes below; the server enforces every access rule. Without --project or --group it uses the project of the current repository.

Terminal window
sfs meeting upload standup.vtt --title "Standup" --date 2026-10-29 --extract
sfs meeting upload --stdin --title "Planning" --group pg_abc123 < notes.txt
sfs meeting list --query roadmap
sfs meeting show meet_0123456789abcdef # numbered transcript, participants, items
sfs meeting resolve-participant meet_… mp_… --persona atlas
sfs meeting resolve-participant meet_… --file people.txt # mp_…,user:<id> | persona:<name> | contact:<id> per line
sfs meeting promote meet_… mi_… --assignee atlas --due 2026-11-05 # shows the draft, asks to confirm
sfs meeting promote meet_… mi_… --target-project proj_… --yes # group meeting, no prompt
sfs meeting link meet_… mi_… tk_… # tie to an existing ticket (asks first)
sfs meeting link meet_… mi_… # use the item's suggestion
sfs meeting unlink meet_… mi_…

A file upload is stored with source=file_upload; --stdin uses manual_paste. .vtt and .srt cue timing is stripped server-side either way. promote always prints the ticket draft first and asks before creating it unless --yes is given; link likewise shows the ticket and the comment it will add before linking. action-items lists each pending item’s suggestion and each linked item’s ticket. See the CLI reference for every command.

Seven tools cover the same flows for agents: upload_meeting_transcript, list_meeting_transcripts, get_meeting_transcript, extract_meeting_action_items, promote_meeting_action_item, link_meeting_action_item and search_meetings. A project is picked by git_remote or the workspace, a group by group_id (user keys only). promote_meeting_action_item and link_meeting_action_item are preview only unless confirm=true; confirming a link also needs the ticket_id the preview returned. Extraction (extract_meeting_action_items, or extract=true on upload) uses your own LLM key. See MCP Server. Chat threads use the same tools; see Chat threads.

The session helper can read meetings you have access to: search_meetings, list_recent_meetings, get_meeting (numbered lines) and list_my_meeting_actions (pending action items owned by you or by a persona that represents you). It is read-only, honours the conversation’s scope, and cites passages as [meeting:<id> L<first>-<last>], which are checked against the meeting’s real line count before the answer is shown.

DELETE soft-deletes a meeting. A scheduled retention sweep hard-deletes:

  • live meetings older than their org’s general.meeting_retention_days (default 180 days, configurable 1–3650), and
  • soft-deleted meetings older than 30 days.

Participants and action items are deleted with the meeting. Uploads, deletes and manual overrides are recorded in the org audit trail (ids and counts only — never transcript text); personal meetings are audited too, with a null org_id.

Method Path Auth Description
POST /api/v1/projects/{project_id}/meetings meetings:write Upload a project meeting
GET /api/v1/projects/{project_id}/meetings meetings:read List project meetings, newest first (?q= substring search); each entry carries extraction_status, extraction_error, pending_action_count and decision_count
POST /api/v1/groups/{group_id}/meetings user key, group editor/admin Upload a group meeting
GET /api/v1/groups/{group_id}/meetings user key, group reader+ List group meetings, newest first (?q=); same entry fields as the project list
GET /api/v1/meetings/{meeting_id} meetings:read Get a meeting with content, participants and action items
DELETE /api/v1/meetings/{meeting_id} meetings:write Soft-delete a meeting
PUT /api/v1/meetings/{meeting_id}/participants/{participant_id} user key Manually override a participant
POST /api/v1/meetings/{meeting_id}/participants/resolve-bulk user key Override up to 100 participants at once (body {"participants": [{"participant_id", one of "user_id" / "persona_name" / "contact_id"}]}), all or nothing: any refused item means nothing changes and a 422 bulk_item_failed lists each item. Works for chat threads with ?kind=chat
POST /api/v1/meetings/{meeting_id}/extract meetings:write Extract participants and action items (returns dropped_items)
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/promote/preview meetings:write Draft a ticket from an action item
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/promote meetings:write Promote an action item to a ticket (agent_tickets)
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/dismiss meetings:write Dismiss a pending action item
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/restore meetings:write Restore a dismissed action item
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/link/preview meetings:write + tickets:write The ticket and the comment Link would add (body {"ticket_id"}, omitted = the item’s suggestion); writes nothing
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/link meetings:write + tickets:write Link a pending item to an existing ticket (comment + timeline event); 409 item_not_pending if it is no longer pending
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/unlink meetings:write + tickets:write Put a linked item back to pending (409 item_not_linked otherwise)
GET /api/v1/meetings/{meeting_id}/linkable-tickets meetings:write + tickets:write Tickets an item can be linked to (?q= title or id prefix, limit ≤ 50); cancelled tickets left out
POST /api/v1/meetings/{meeting_id}/action-items/{item_id}/dismiss-suggestion meetings:write Hide the item’s suggested ticket (Not this one); the item stays pending
POST /api/v1/projects/{project_id}/meetings/{meeting_id}/items/{item_id}/reminder reminders:write Create the reminder an item suggests
POST /api/v1/groups/{group_id}/meetings/{meeting_id}/items/{item_id}/reminder user key, group reader+ The same for a group meeting
GET /api/v1/projects/{project_id}/meetings/{meeting_id}/reminders reminders:read Your reminders on a project meeting and its items
GET /api/v1/groups/{group_id}/meetings/{meeting_id}/reminders user key, group reader+ Your reminders on a group meeting and its items
GET /api/v1/orgs/{org_id}/settings # includes meeting_retention_days (null = platform default 180)
PUT /api/v1/orgs/{org_id}/settings # {"meeting_retention_days": 365}