{"openapi":"3.1.0","info":{"title":"Sentra Public API","description":"Programmatic access to Sentra meetings, entities, and facts.\n\n**Authentication.** Every request must carry your Sentra API key as ``Authorization: Bearer sk_sentra_…``. Generate a key from Settings → API Access in the Sentra app. The raw value is shown once at creation — store it like any other secret. Keys inherit the owning user's meeting access and organization scope.\n\n**API host.** ``https://api.sentra.app``. Every path in this document is absolute and begins with ``/external/v1`` — the canonical version namespace. There is no ``/v1`` prefix. The OpenAPI document itself lives at ``/external/v1/openapi.json``.\n\n**Rate limits.** Per-key, default 120 requests/minute for reads. Bursts above the budget receive ``429 Too Many Requests``; back off and retry.","version":"2026.11.03"},"servers":[{"url":"https://api.sentra.app"}],"paths":{"/external/v1/meetings":{"get":{"tags":["external-v1"],"summary":"List Meetings","description":"List meetings visible to the API key's user. Use `search` to filter by keyword instead of paginating blindly. Admins see org meetings except private ones they did not attend; non-admins see meetings they attended.","operationId":"list_meetings_external_v1_meetings_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter meetings created at or after this UTC timestamp.","title":"Start Date"},"description":"Filter meetings created at or after this UTC timestamp."},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Filter meetings created at or before this UTC timestamp.","title":"End Date"},"description":"Filter meetings created at or before this UTC timestamp."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Case-insensitive exact substring search within meeting names only.","title":"Search"},"description":"Case-insensitive exact substring search within meeting names only."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/MeetingStatus"}},{"type":"null"}],"description":"Filter by meeting status. Repeat `status` to pass multiple values. `skipped` represents bot meetings where Sentra did not capture a usable recording/transcript (e.g. bot not admitted, or no meaningful captured content). It does not imply the user did not attend.","title":"Status"},"description":"Filter by meeting status. Repeat `status` to pass multiple values. `skipped` represents bot meetings where Sentra did not capture a usable recording/transcript (e.g. bot not admitted, or no meaningful captured content). It does not imply the user did not attend."},{"name":"attendee","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":320},{"type":"null"}],"description":"Filter to meetings that include this attendee email.","title":"Attendee"},"description":"Filter to meetings that include this attendee email."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/meetings/{meeting_id}":{"get":{"tags":["external-v1"],"summary":"Get Meeting","description":"Return summary and full transcript for one meeting by Recall bot id (`meeting_id`). Expensive — returns large payloads. Call at most once per turn unless the user explicitly asks for multiple meetings. Prefer `get_entity_chunks` for cross-meeting questions about people or companies.","operationId":"get_meeting_external_v1_meetings__meeting_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"meeting_id","in":"path","required":true,"schema":{"type":"string","title":"Meeting Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingDetail"}}}},"409":{"description":"Meeting is still being processed; transcript not yet available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingProcessingResponse"}}}},"404":{"description":"Meeting not found or caller is not allowed to read it."},"502":{"description":"Transcript storage is unreachable; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingUpstreamErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/emails/threads/raw":{"get":{"tags":["external-v1"],"summary":"Get Raw Email Thread","description":"Fetch every message in an email thread that the calling API key is allowed to see, with raw bodies and original RFC822 headers. Paginated.\n\nTwo layered access checks apply, both enforced server-side:\n1. The API-key owner must be assigned to a data-access role that carries the ``email_raw`` capability marker. Without it the request returns 403 even if the caller can otherwise read every message in the thread via the standard email endpoints.\n2. Within the thread, only messages whose participant set (sender + To + Cc) intersects the address set granted by the caller's ``email`` data-access rules are returned. Messages the caller cannot see under the standard email visibility model are filtered out silently — the message count the caller sees may be smaller than the actual thread length stored.\n\nPagination is keyset (cursor + limit). Pass the ``next_cursor`` from each response back as the ``cursor`` query parameter to fetch the next page; iteration ends when ``next_cursor`` is null. A page can be empty (``message_count=0``) while still carrying a non-null ``next_cursor`` — that signals a stretch of rows the caller is not allowed to see, and the caller should follow the cursor.\n\nReturns 404 only on the FIRST page (no cursor) when no message in the thread is visible to the caller, regardless of whether the thread exists in the org. Returns 400 on a malformed cursor.","operationId":"get_raw_email_thread_external_v1_emails_threads_raw_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"thread_key","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Normalized RFC822 Message-ID root identifying the thread. Carried as a query parameter — not a path segment — because Message-IDs legitimately contain ``/``, ``?``, ``#``, ``&``, ``+``, and ``=``, which break path-segment routing across the ASGI / proxy stack.","title":"Thread Key"},"description":"Normalized RFC822 Message-ID root identifying the thread. Carried as a query parameter — not a path segment — because Message-IDs legitimately contain ``/``, ``?``, ``#``, ``&``, ``+``, and ``=``, which break path-segment routing across the ASGI / proxy stack."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque pagination cursor. Omit (or null) to fetch the first page; otherwise pass back the ``next_cursor`` value from the prior response unchanged.","title":"Cursor"},"description":"Opaque pagination cursor. Omit (or null) to fetch the first page; otherwise pass back the ``next_cursor`` value from the prior response unchanged."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Maximum messages to fetch from storage for this page. The visible portion of the page (after the per-message ACL filter) may be smaller — possibly zero — without implying the thread has ended; follow ``next_cursor`` until it is null. Defaults to 50, capped at 200.","default":50,"title":"Limit"},"description":"Maximum messages to fetch from storage for this page. The visible portion of the page (after the per-message ACL filter) may be smaller — possibly zero — without implying the thread has ended; follow ``next_cursor`` until it is null. Defaults to 50, capped at 200."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RawEmailThreadResponse"}}}},"400":{"description":"Cursor argument is malformed. The cursor is opaque to the client; pass back the ``next_cursor`` value from the prior response unmodified."},"403":{"description":"Caller's API key does not carry the ``email_raw`` data-access capability. The endpoint surface itself is gated separately from per-message email access."},"404":{"description":"First page only: thread not found OR caller is not allowed to read any message in it. The two cases are intentionally indistinguishable to avoid disclosing thread existence to a caller who has no access. Subsequent pages never 404 — an empty terminal page is returned with ``next_cursor=null``."},"502":{"description":"Email body storage was unreachable while assembling the response. Retry with backoff."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/followup-burden":{"get":{"tags":["external-v1"],"summary":"List Followup Burden","description":"Latest follow-up-burden snapshot for every configured target the calling API key is allowed to see, plus a ``cohort`` block aggregating those visible targets (totals, ratios, and blocker-score summary). Requires the ``email_raw`` data-access capability (403 otherwise). Targets are filtered to those the caller's ``email`` access covers — an org-wide (ALL) key sees everyone; a scoped key sees only addresses it is granted, and its cohort reflects that subset. Items are ordered by ``blocker_score`` (highest-blocking first; unscored last), then by confirmed chases. Results are PAGED: ``items`` holds at most ``limit`` targets (default 100, max 500) starting at ``offset``, while ``total`` is the full visible count and ``has_more`` says whether more pages remain — page on with ``offset = offset + count``. ``cohort`` always covers ALL visible targets, not just the current page, so cohort ratios like ``chased_ask_ratio`` are complete from the first page and need no paging to total up. Pass ``as_of=YYYY-MM-DD`` to get each target's newest snapshot on or before that date (a past week's view); omit for today's latest. Follow the per-user endpoint and then the raw-thread endpoint for evidence.","operationId":"list_followup_burden_external_v1_followup_burden_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"as_of","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Optional ISO date (YYYY-MM-DD). Returns each target's newest snapshot on or before this date — a past week's view. Omit for today's latest. The ``cohort`` block reflects the same set.","title":"As Of"},"description":"Optional ISO date (YYYY-MM-DD). Returns each target's newest snapshot on or before this date — a past week's view. Omit for today's latest. The ``cohort`` block reflects the same set."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Max targets to return in ``items``. ``cohort`` and ``total`` always cover every visible target, not just this page.","default":100,"title":"Limit"},"description":"Max targets to return in ``items``. ``cohort`` and ``total`` always cover every visible target, not just this page."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Targets to skip before this page.","default":0,"title":"Offset"},"description":"Targets to skip before this page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowupBurdenSummaryResponse"}}}},"403":{"description":"Caller's API key does not carry the ``email_raw`` data-access capability."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/followup-burden/user":{"get":{"tags":["external-v1"],"summary":"Get Followup Burden User","description":"One target's follow-up-burden snapshot plus the evidence thread_keys (the threads the target was chased in). Requires the ``email_raw`` capability. Pass ``as_of=YYYY-MM-DD`` for that target's newest snapshot on or before that date (a past week's view); omit for the latest. Returns 404 when the target is not configured, has no snapshot by the requested date, or the caller's email access does not cover that person (the three are intentionally indistinguishable). The returned ``evidence_thread_keys`` are fetched from ``GET /emails/threads/raw?thread_key=...``, which independently enforces per-message visibility — so a thread_key here never grants content the caller could not already read.","operationId":"get_followup_burden_user_external_v1_followup_burden_user_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","minLength":3,"description":"Target email address (carried as a query parameter because addresses can contain ``+`` and other characters that break path-segment routing). Normalized server-side.","title":"Email"},"description":"Target email address (carried as a query parameter because addresses can contain ``+`` and other characters that break path-segment routing). Normalized server-side."},{"name":"as_of","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Optional ISO date (YYYY-MM-DD). Returns the target's newest snapshot on or before this date — a past week's view. Omit for the latest.","title":"As Of"},"description":"Optional ISO date (YYYY-MM-DD). Returns the target's newest snapshot on or before this date — a past week's view. Omit for the latest."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowupBurdenUserResponse"}}}},"403":{"description":"Caller's API key does not carry the ``email_raw`` data-access capability."},"404":{"description":"Target is not configured, has no snapshot yet, or the caller's email access does not cover that person. The three are intentionally indistinguishable."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/thread-stalls":{"get":{"tags":["external-v1"],"summary":"List Thread Stalls","description":"Latest snapshot of the organization's large, stalling/blocking business email threads, ranked by a blend of criticality (LLM-scored impact / urgency / scope with aging) and stall severity (how long a big, multi-party thread has gone quiet). Automation/noise (monitoring alerts, recurring digests, single-sender broadcasts) is filtered out. Requires the ``email_raw`` data-access capability (403 otherwise); rows are limited to threads the caller's email access covers. Each row's ``status`` is ``OPEN``, ``CLOSED`` (resolved), or ``UNKNOWN`` (scoring failed for that thread). Open threads rank above closed ones. Each row carries a ``thread_key`` for the raw-thread endpoint, which independently enforces per-message visibility. Pass ``status=open`` to drop CLOSED threads (it keeps OPEN and UNKNOWN), or ``as_of=YYYY-MM-DD`` for a past day's snapshot; omit for today's latest.","operationId":"list_thread_stalls_external_v1_thread_stalls_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional. ``open`` hides resolved (closed) threads; omit for all.","title":"Status"},"description":"Optional. ``open`` hides resolved (closed) threads; omit for all."},{"name":"as_of","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Optional ISO date (YYYY-MM-DD). Returns the newest snapshot on or before this date; omit for today's latest.","title":"As Of"},"description":"Optional ISO date (YYYY-MM-DD). Returns the newest snapshot on or before this date; omit for today's latest."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThreadStallListResponse"}}}},"403":{"description":"Caller's API key does not carry the ``email_raw`` data-access capability."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/emails/threads":{"get":{"tags":["external-v1"],"summary":"List Email Threads For Address","description":"List the unique email threads a given address participates in that the calling API key is allowed to see, most-recent first. Metadata only (subject, participants, dates, counts) — no message bodies; use ``GET /external/v1/emails/threads/raw`` with a returned ``thread_key`` to fetch raw bodies for a single thread.\n\n``email_address`` is required. Two layered access checks apply, both server-side:\n1. The API-key owner must carry the ``email_raw`` data-access capability (403 otherwise) — the same gate as the raw-thread endpoint.\n2. Only threads containing at least one message the caller can see under their ``email`` data-access rules are returned; a thread's metadata (participants, count, latest date/subject) reflects only the messages the caller can see. If the caller has no access to the address's mail, the list is empty (this does not reveal whether the address has any mail). If the requested ``email_address`` is a user the caller has REVOKED, the list is always empty — a revoked user's threads are never surfaced when querying for that user, even shared-org threads.\n\nBy default only business-context / business-critical classified threads are returned (the unfiltered corpus is too noisy for an audit list); set ``business_only=false`` to include every classification.\n\nPagination is keyset (cursor + limit). Pass ``next_cursor`` back as ``cursor`` until it is null.","operationId":"list_email_threads_for_address_external_v1_emails_threads_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"email_address","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":320,"description":"Required. The target participant address whose threads to list. Normalized (lowercased, display name stripped) before use; a value that does not parse to an address returns 400.","title":"Email Address"},"description":"Required. The target participant address whose threads to list. Normalized (lowercased, display name stripped) before use; a value that does not parse to an address returns 400."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque pagination cursor. Omit (or null) for the first page; otherwise pass back the prior response's ``next_cursor`` unchanged. Iteration ends when ``next_cursor`` is null.","title":"Cursor"},"description":"Opaque pagination cursor. Omit (or null) for the first page; otherwise pass back the prior response's ``next_cursor`` unchanged. Iteration ends when ``next_cursor`` is null."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Maximum threads per page. Defaults to 50, capped at 200.","default":50,"title":"Limit"},"description":"Maximum threads per page. Defaults to 50, capped at 200."},{"name":"business_only","in":"query","required":false,"schema":{"type":"boolean","description":"When true (default), only business-context / business-critical classified threads are returned — the unfiltered corpus (newsletters, automated noise, etc.) is too noisy for an audit list. Set false to include every classification.","default":true,"title":"Business Only"},"description":"When true (default), only business-context / business-critical classified threads are returned — the unfiltered corpus (newsletters, automated noise, etc.) is too noisy for an audit list. Set false to include every classification."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailThreadListResponse"}}}},"400":{"description":"``email_address`` is present but not a valid address, or the ``cursor`` is malformed. The cursor is opaque; pass back the ``next_cursor`` value from the prior response unmodified."},"403":{"description":"Caller's API key does not carry the ``email_raw`` data-access capability — the same gate as ``/emails/threads/raw``."},"422":{"description":"The required ``email_address`` query parameter is missing (FastAPI request-validation error)."}}}},"/external/v1/sources":{"get":{"tags":["external-v1"],"summary":"List Sources","description":"List the data sources the org can produce data from. Use this to discover (1) which values the `source_types` filter on `get_entity_chunks` / `get_entity_relationships` accepts and (2) which external integrations the org has connected. `meeting`, `file`, and `manual` are always `connected=true`; `slack`, `github`, `linear`, `clickup`, `jira`, `erpnext`, and every HubSpot object-type source (`hubspot_contact` / `hubspot_company` / `hubspot_deal` / `hubspot_ticket` / `hubspot_note` / `hubspot_task` / `hubspot_call` / `hubspot_meeting` / `hubspot_lead` / `hubspot_line_item` / `hubspot_quote` / `hubspot_custom_object`) are `true` iff the org has connected that integration and has synced records of that type (`erpnext` requires only that the integration is connected). ORG-LEVEL signal — a `connected=true` source can still return zero rows for the calling user if they aren't a member of the underlying Slack channel / Linear team / GitHub repo / ClickUp workspace, or lack `BROWSE_PROJECTS` on the Jira issue's project, hold no ERPNext permission on the document, or aren't the HubSpot record's owner / on its team (or, for `manual`, if no member of the org has authored a chunk visible to them).","operationId":"list_sources_external_v1_sources_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceListResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/external/v1/entities":{"get":{"tags":["external-v1"],"summary":"List Entities","description":"List entities (people, companies, projects, etc.) the API key's user can see, grouped by type. Visibility is per-user: two keys in the same org may return different lists. Use `entity_type` to filter by type and `search` to find entities by name; use `cursor` for pagination. `search` is case-insensitive and matches both the entity's name (canonical_name) and its aliases (other names, spellings and abbreviations recorded for the entity). With `search`, the entities in each group are ranked best match first, and each one carries `match_type` and `matched_text` (the name or alias that matched). The match types, from strongest to weakest:\n- `substring`: the search text is part of the name or of an alias.\n- `compact`: the search text matches when spaces and punctuation are ignored (for example \"AcmeCorp\" and \"Acme Corp\").\n- `token`: every word of the search text is in the name or in one alias, in any order (for example \"Smith John\" and \"John Smith\").\n- `fuzzy`: the name or alias is similar to the search text but not the same (for example another spelling). A `fuzzy` match is a CANDIDATE, not a confirmed match: it is often a different entity with a similar name (for example \"Daniel\" and \"Danielle\"). Before you use a fuzzy match, read its chunks and confirm that they are about what you are looking for. Fuzzy matches always rank below the other match types. There are no fuzzy matches when the search text contains a digit.\n\nWith `search`, when `total_is_lower_bound` is true, the search stopped early and there can be more matches than `total_entities`: use `cursor` for the next page. Each returned entity carries a UUID `id` that you must pass verbatim to `get_entity_chunks` / `get_entity_relationships` / `fetch entity:<id>`. Each entity carries a `status` you MUST respect: `confirmed` means it is likely to be a real, valid entity. `proposed` means the OPPOSITE — it is an UNCONFIRMED, hypothetical suggestion the org has NOT yet accepted (e.g. a `proposed` project is NOT a real project on the roadmap; it is a candidate a human still has to approve). NEVER present a `proposed` entity as a confirmed fact: if you mention one, you MUST explicitly label it as proposed / unconfirmed / not yet accepted, and prefer `confirmed` entities as the source of truth. `rejected` means a human explicitly declined it — do not rely on it.","operationId":"list_entities_external_v1_entities_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Filter by entity type slug. Repeat `entity_type` to pass multiple types.","title":"Entity Type"},"description":"Filter by entity type slug. Repeat `entity_type` to pass multiple types."},{"name":"updated_since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only include entities with at least one visible chunk at or after this timestamp. A chunk's time is when its source content happened (a message, a meeting), not when Sentra ingested it. A synced document's chunks carry its last-modified time when its source reported one (so a newly synced older document can add chunks before the bound); otherwise they carry the time Sentra ingested the document. **Must be timezone-aware** (include `Z` or an explicit offset like `+00:00`); naive datetimes are rejected with an actionable error.","title":"Updated Since"},"description":"Only include entities with at least one visible chunk at or after this timestamp. A chunk's time is when its source content happened (a message, a meeting), not when Sentra ingested it. A synced document's chunks carry its last-modified time when its source reported one (so a newly synced older document can add chunks before the bound); otherwise they carry the time Sentra ingested the document. **Must be timezone-aware** (include `Z` or an explicit offset like `+00:00`); naive datetimes are rejected with an actionable error."},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":200,"minimum":1},{"type":"null"}],"description":"Optional page size for cursor pagination.","title":"Limit"},"description":"Optional page size for cursor pagination."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor returned by the previous entities response.","title":"Cursor"},"description":"Opaque cursor returned by the previous entities response."},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Optional case-insensitive search on the entity's `canonical_name` and its aliases: a substring, the same text with spaces and punctuation ignored, all words in any order, or a similar spelling (`fuzzy`), ranked best match first in each group. Whitespace-only values are treated as no filter. Mirrors the in-app `/entity-memory/entities` route and the desktop Spotlight typeahead so the public API, the in-app surface, and the Spotlight share one search contract.","title":"Search"},"description":"Optional case-insensitive search on the entity's `canonical_name` and its aliases: a substring, the same text with spaces and punctuation ignored, all words in any order, or a similar spelling (`fuzzy`), ranked best match first in each group. Whitespace-only values are treated as no filter. Mirrors the in-app `/entity-memory/entities` route and the desktop Spotlight typeahead so the public API, the in-app surface, and the Spotlight share one search contract."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/entities/important":{"get":{"tags":["external-v1"],"summary":"List Important Entities","description":"Return the user's important entities (projects, companies, topics, and the like — not people) as an unranked set: each item carries the entity id, type, and name, and the items are sorted by name. The set carries no scores and no ranks by design — the underlying scores are computed from org-wide data, so exposing them would leak activity the caller cannot see. Visibility is per-user: a user sees only entities they can access. This is a bounded top-N read: `count` is the number of returned items, and there is no total or pagination cursor.","operationId":"list_important_entities_external_v1_entities_important_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"The maximum number of entities to return (1-100).","default":20,"title":"Limit"},"description":"The maximum number of entities to return (1-100)."},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Restrict to these entity type slugs.","title":"Entity Type"},"description":"Restrict to these entity type slugs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportantEntitiesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/entities/types":{"get":{"tags":["external-v1"],"summary":"List Entity Types","description":"Return the org's configured entity type slugs and display names. Cheap lookup to resolve a slug from `list_entities`.","operationId":"list_entity_types_external_v1_entities_types_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityTypeListResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/external/v1/entities/{entity_id}/chunks":{"get":{"tags":["external-v1"],"summary":"Get Entity Chunks","description":"Return ACL-filtered fact chunks for one entity. Prefer this over `get_meeting` for cross-meeting questions — chunks are concise and include source citations. `entity_id` must be a UUID returned by `list_entities` (not a display name or slug); malformed values are rejected with an actionable error.","operationId":"get_entity_chunks_external_v1_entities__entity_id__chunks_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Entity Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Page size; max 100.","default":10,"title":"Limit"},"description":"Page size; max 100."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"source_types","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Optional filter restricting chunks to a subset of source types. Repeat `source_types` to pass multiple values. Omit to return rows from every source the caller can see. Unknown values are rejected with an actionable error. Allowed values: ['basecamp', 'chat_update', 'clickup', 'confluence', 'email', 'erpnext', 'file', 'github', 'google_chat', 'hubspot_call', 'hubspot_company', 'hubspot_contact', 'hubspot_custom_object', 'hubspot_deal', 'hubspot_lead', 'hubspot_line_item', 'hubspot_meeting', 'hubspot_note', 'hubspot_quote', 'hubspot_task', 'hubspot_ticket', 'jira', 'linear', 'manual', 'meeting', 'personal_email', 'slack', 'teams_channel', 'teams_chat', 'zendesk'].","title":"Source Types"},"description":"Optional filter restricting chunks to a subset of source types. Repeat `source_types` to pass multiple values. Omit to return rows from every source the caller can see. Unknown values are rejected with an actionable error. Allowed values: ['basecamp', 'chat_update', 'clickup', 'confluence', 'email', 'erpnext', 'file', 'github', 'google_chat', 'hubspot_call', 'hubspot_company', 'hubspot_contact', 'hubspot_custom_object', 'hubspot_deal', 'hubspot_lead', 'hubspot_line_item', 'hubspot_meeting', 'hubspot_note', 'hubspot_quote', 'hubspot_task', 'hubspot_ticket', 'jira', 'linear', 'manual', 'meeting', 'personal_email', 'slack', 'teams_channel', 'teams_chat', 'zendesk']."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityChunksResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/entities/{entity_id}/relationships":{"get":{"tags":["external-v1"],"summary":"Get Entity Relationships","description":"Return extracted links and co-attendance for one entity. Co-attendance defaults to the last 180 days. `entity_id` must be a UUID returned by `list_entities`; malformed values are rejected with an actionable error.","operationId":"get_entity_relationships_external_v1_entities__entity_id__relationships_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Entity Id"}},{"name":"max_links","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Page size for LLM-extracted link rows. Pagination is cursor-based: the response carries `next_links_cursor` when more rows are available — pass it back as `links_cursor` to fetch the next page.","default":50,"title":"Max Links"},"description":"Page size for LLM-extracted link rows. Pagination is cursor-based: the response carries `next_links_cursor` when more rows are available — pass it back as `links_cursor` to fetch the next page."},{"name":"links_cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor returned as `next_links_cursor` on a previous response. Omit to fetch the first page. Malformed cursors are rejected with an actionable error.","title":"Links Cursor"},"description":"Opaque cursor returned as `next_links_cursor` on a previous response. Omit to fetch the first page. Malformed cursors are rejected with an actionable error."},{"name":"max_counterparts","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Max Counterparts"}},{"name":"max_events_per_counterpart","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Max Events Per Counterpart"}},{"name":"co_attendance_since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Inclusive lower bound on source-event time for co-attendance aggregation. **Must be timezone-aware** (RFC 3339 with `Z` or an explicit offset like `+00:00`); naive datetimes are rejected with an actionable error. Defaults to 180 days before `co_attendance_until` (or before now if `co_attendance_until` is also omitted). Does NOT affect extracted links.","title":"Co Attendance Since"},"description":"Inclusive lower bound on source-event time for co-attendance aggregation. **Must be timezone-aware** (RFC 3339 with `Z` or an explicit offset like `+00:00`); naive datetimes are rejected with an actionable error. Defaults to 180 days before `co_attendance_until` (or before now if `co_attendance_until` is also omitted). Does NOT affect extracted links."},{"name":"co_attendance_until","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Inclusive upper bound on source-event time for co-attendance aggregation. **Must be timezone-aware** when specified; naive datetimes are rejected with an actionable error. Omit for no upper bound.","title":"Co Attendance Until"},"description":"Inclusive upper bound on source-event time for co-attendance aggregation. **Must be timezone-aware** when specified; naive datetimes are rejected with an actionable error. Omit for no upper bound."},{"name":"source_types","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Optional filter restricting links AND co-attendance to a subset of source types. Repeat `source_types` to pass multiple values. Omit to return rows from every source the caller can see. Unknown values are rejected with an actionable error. Allowed values: ['basecamp', 'chat_update', 'clickup', 'confluence', 'email', 'erpnext', 'file', 'github', 'google_chat', 'hubspot_call', 'hubspot_company', 'hubspot_contact', 'hubspot_custom_object', 'hubspot_deal', 'hubspot_lead', 'hubspot_line_item', 'hubspot_meeting', 'hubspot_note', 'hubspot_quote', 'hubspot_task', 'hubspot_ticket', 'jira', 'linear', 'manual', 'meeting', 'personal_email', 'slack', 'teams_channel', 'teams_chat', 'zendesk']. Co-attendance is only populated by the meeting adapter today, so a filter that excludes `meeting` returns an empty `co_attendance` list.","title":"Source Types"},"description":"Optional filter restricting links AND co-attendance to a subset of source types. Repeat `source_types` to pass multiple values. Omit to return rows from every source the caller can see. Unknown values are rejected with an actionable error. Allowed values: ['basecamp', 'chat_update', 'clickup', 'confluence', 'email', 'erpnext', 'file', 'github', 'google_chat', 'hubspot_call', 'hubspot_company', 'hubspot_contact', 'hubspot_custom_object', 'hubspot_deal', 'hubspot_lead', 'hubspot_line_item', 'hubspot_meeting', 'hubspot_note', 'hubspot_quote', 'hubspot_task', 'hubspot_ticket', 'jira', 'linear', 'manual', 'meeting', 'personal_email', 'slack', 'teams_channel', 'teams_chat', 'zendesk']. Co-attendance is only populated by the meeting adapter today, so a filter that excludes `meeting` returns an empty `co_attendance` list."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityRelationshipsExternalResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/memory-agent/runs":{"post":{"tags":["external-v1"],"summary":"Start Memory Agent Run","description":"Start an asynchronous memory-agent run.\nThe agent investigates your prompt over your Sentra memory and returns a written answer with source citations. The run uses your API key's identity and memory access.\nCaller should prefer this tool over other memory tools for complex, multi-step, or open-ended questions about Sentra memory: it runs a full research agent that plans, searches across sources, and synthesizes an answer, so it handles questions that a single lookup cannot.  \nRuns typically take a few minutes and can run up to about 30 minutes, so this returns a job_id immediately; caller must then call GET /memory-agent/runs/{job_id} repeatedly. Status is one of 'queued', 'running', 'completed', or 'failed': 'queued' and 'running' are in-flight (keep polling), 'completed' and 'failed' are terminal (stop). Do not stop after a single call - the run starts in 'queued' and only reaches a terminal status once a worker finishes it. \nWhile status is non-terminal the run is still working as long as the result's 'elapsed_seconds' keeps increasing and 'last_updated_at' keeps advancing; do not treat a non-terminal status as a timeout. \nPoll no more than once every 10s. Polling shares the same 120 req/min per-API-key rate limit as every other public read, so a tight poll loop can exhaust the bucket for your unrelated reads too - back off accordingly.","operationId":"start_memory_agent_run_external_v1_memory_agent_runs_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemoryAgentRunRequest"}}},"required":true},"responses":{"202":{"description":"Run accepted and queued. The Retry-After header gives the recommended seconds to wait before the first poll. Polling shares the same 120 req/min per-API-key limit as all public reads, so honor the header to avoid 429s on unrelated calls.","headers":{"Retry-After":{"description":"Recommended seconds to wait before the first poll.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemoryAgentRunAcceptedResponse"}}}},"422":{"description":"The prompt is empty (after trimming whitespace and stripping invisible Unicode characters) or exceeds the maximum length of 4000 characters."},"503":{"description":"The memory agent could not accept the run (backing store down or task dispatch failed). The Retry-After header gives the recommended seconds to wait before retrying; honor it rather than retrying immediately.","headers":{"Retry-After":{"description":"Recommended seconds to wait before retrying.","schema":{"type":"integer"}}}}},"security":[{"BearerAuth":[]}]}},"/external/v1/memory-agent/runs/{job_id}":{"get":{"tags":["external-v1"],"summary":"Get Memory Agent Run","description":"Fetch the current status and result of a memory-agent run started with the start endpoint. \nGET /memory-agent/runs/{job_id} returns immediately; while status is 'queued' or 'running' the run is not finished yet - re-call after about 10s. \nEvery response includes timing fields: 'started_at' (ISO-8601 UTC), 'last_updated_at' (ISO-8601 UTC, bumped by a worker heartbeat while the run executes), and 'elapsed_seconds' (integer seconds since the run started). While the run is in flight 'elapsed_seconds' increases on every poll and 'last_updated_at' advances every few seconds as the worker heartbeats: as long as these keep moving the run is alive and working - keep polling and do not assume it timed out just because there is no answer yet. \nRuns can take up to about 30 minutes; if 'last_updated_at' stops advancing for well over a minute while status is still non-terminal, the run may be stuck. The final_response and citations are present only when status is 'completed'. Records expire 2 hours after creation. \nPolling counts against the shared 120 req/min per-key rate limit, so respect the cadence rather than busy-looping.","operationId":"get_memory_agent_run_external_v1_memory_agent_runs__job_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Current run status. While status is 'queued' or 'running' a Retry-After header gives the recommended seconds before the next poll; it is absent on terminal ('completed'/'failed') responses. Polling shares the 120 req/min per-API-key limit with all public reads.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemoryAgentRunStatusResponse"}}},"headers":{"Retry-After":{"description":"Recommended seconds before the next poll. Present only while the run is non-terminal.","schema":{"type":"integer"}}}},"404":{"description":"Run not found, expired, or not owned by the caller."},"503":{"description":"The polling store is temporarily unavailable. The Retry-After header gives the recommended seconds to wait before retrying.","headers":{"Retry-After":{"description":"Recommended seconds to wait before retrying.","schema":{"type":"integer"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes":{"get":{"tags":["external-v1"],"summary":"List Swimlanes Endpoint","description":"List the organization's swimlanes (org-level project views over memory), each with the API key user's cached live-summary when one exists. Reading the list does not trigger generation.","operationId":"list_swimlanes_endpoint_external_v1_swimlanes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneList"}}}}},"security":[{"BearerAuth":[]}]},"post":{"tags":["external-v1"],"summary":"Create Swimlane Endpoint","description":"Create a swimlane. `title` and `description` are required; `goal`, `owner_user_id`, and `stakeholder_user_ids` are optional. Owner and stakeholders are Sentra user ids — resolve names to ids with `list_org_members` first. Creation triggers asynchronous entity discovery.","operationId":"create_swimlane_endpoint_external_v1_swimlanes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwimlaneBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"BearerAuth":[]}]}},"/external/v1/swimlanes/{swimlane_id}":{"get":{"tags":["external-v1"],"summary":"Get Swimlane Endpoint","description":"Return one swimlane's own attributes (title, description, goal, owner, stakeholders, discovery state) by id. A person with `is_pending_invite: true` is an invited person who has not joined; they are not returned by `list_org_members` and stay on the swimlane when you send their id again in the same list.","operationId":"get_swimlane_endpoint_external_v1_swimlanes__swimlane_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["external-v1"],"summary":"Update Swimlane Endpoint","description":"Update a swimlane's user-authored fields. Every field is optional but at least one must be sent. `stakeholder_user_ids` accepts [] to clear its Sentra-user subset (external, unresolved people are preserved). Resolve ids with `list_org_members`. Changing title/description/goal re-triggers discovery. A person with `is_pending_invite: true` is an invited person who has not joined; they are not returned by `list_org_members` and stay on the swimlane when you send their id again in the same list. A suggested swimlane must keep at least one active (joined) owner or stakeholder; an update that leaves no active one is rejected. Send `owner_user_id: null` to clear the owner.","operationId":"update_swimlane_endpoint_external_v1_swimlanes__swimlane_id__patch","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSwimlaneBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["external-v1"],"summary":"Delete Swimlane Endpoint","description":"Archive (soft-delete) a swimlane for the WHOLE organization — this is destructive and affects every member, not just you. You must pass `confirm=true` to proceed; a call without it is rejected.","operationId":"delete_swimlane_endpoint_external_v1_swimlanes__swimlane_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}},{"name":"confirm","in":"query","required":false,"schema":{"type":"boolean","description":"Must be true to perform this destructive, org-wide archive. Any other value is rejected.","default":false,"title":"Confirm"},"description":"Must be true to perform this destructive, org-wide archive. Any other value is rejected."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneAck"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/activity":{"get":{"tags":["external-v1"],"summary":"List Swimlane Activity Endpoint","description":"Return a swimlane's Event Feed: the notable, real events that happened on the project, each typed (deploy_release, decision, milestone, blocker, stall, ...) and badged hard (observed) or soft (inferred). An empty feed is NOT authoritative ('nothing happened') until it has been generated for you: check `state`, and when it is empty or stale call `refresh_swimlane_activity` to generate it, then re-read. The feed is returned whole, so `limit`/`cursor` are ignored and `next_cursor` is always null — do not paginate. This read returns the latest cached result immediately with a `state` and freshness fields (`is_stale`, `generated_at`); it never starts generation. When `is_stale` is true (or no result exists yet), call the matching `refresh_*` operation to regenerate, then re-read. While a refresh you triggered is in flight, `state` is `generating` — re-read until it settles.","operationId":"list_swimlane_activity_endpoint_external_v1_swimlanes__swimlane_id__activity_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":30,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneActivityPage"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/live-summary":{"get":{"tags":["external-v1"],"summary":"Get Live Summary Endpoint","description":"Return the API key user's live summary for a swimlane (the generated, ACL-filtered latest updates). This read returns the latest cached result immediately with a `state` and freshness fields (`is_stale`, `generated_at`); it never starts generation. When `is_stale` is true (or no result exists yet), call the matching `refresh_*` operation to regenerate, then re-read. While a refresh you triggered is in flight, `state` is `generating` — re-read until it settles.","operationId":"get_live_summary_endpoint_external_v1_swimlanes__swimlane_id__live_summary_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneLiveSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/attention":{"get":{"tags":["external-v1"],"summary":"Get Attention Endpoint","description":"Return the API key user's active attention items for a swimlane (proactive assessments that the swimlane needs attention). Each item has a `type` (`risk` | `contradiction` | `other`); typed items (risk / contradiction) also carry an optional `evidence` payload (why it matters, a primary quote, the event trail, a suggested next step, and — for a contradiction — the two conflicting sides) and a `confidence` score. `confidence` is the detection ensemble's vote share (0-1), NOT the probability the risk is real — surface the evidence, do not read it as a truth likelihood. This read returns the latest cached result immediately with a `state` and freshness fields (`is_stale`, `generated_at`); it never starts generation. When `is_stale` is true (or no result exists yet), call the matching `refresh_*` operation to regenerate, then re-read. While a refresh you triggered is in flight, `state` is `generating` — re-read until it settles.","operationId":"get_attention_endpoint_external_v1_swimlanes__swimlane_id__attention_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneAttentionItems"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/whats-next":{"get":{"tags":["external-v1"],"summary":"Get Whats Next Endpoint","description":"Return the API key user's forward-looking items for a swimlane. This capability is in active development: an empty set is NOT authoritative ('nothing coming up') until the finder ships. This read returns the latest cached result immediately with a `state` and freshness fields (`is_stale`, `generated_at`); it never starts generation. When `is_stale` is true (or no result exists yet), call the matching `refresh_*` operation to regenerate, then re-read. While a refresh you triggered is in flight, `state` is `generating` — re-read until it settles.","operationId":"get_whats_next_endpoint_external_v1_swimlanes__swimlane_id__whats_next_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneWhatsNextItems"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/org/members":{"get":{"tags":["external-v1"],"summary":"List Org Members Endpoint","description":"List the members of the API key's organization as `{id, display_name}`. Use this to resolve a person's name to the numeric `id` the swimlane create/update contract requires for `owner_user_id` and `stakeholder_user_ids`. Pass `search` to filter by name or email instead of paginating the whole roster. Work emails are omitted by default; pass `include_email=true` only when you must disambiguate members who share a display name.","operationId":"list_org_members_endpoint_external_v1_org_members_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive substring match against member first name, last name, and email. Omit to list the whole org (paginated).","title":"Search"},"description":"Case-insensitive substring match against member first name, last name, and email. Omit to list the whole org (paginated)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"include_email","in":"query","required":false,"schema":{"type":"boolean","description":"Include each member's work email in the response. Off by default to avoid broadcasting the roster's emails; turn on only to tell apart members who share a display name.","default":false,"title":"Include Email"},"description":"Include each member's work email in the response. Off by default to avoid broadcasting the roster's emails; turn on only to tell apart members who share a display name."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgMemberDirectory"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/discovery/retry":{"post":{"tags":["external-v1"],"summary":"Retry Discovery Endpoint","description":"Re-trigger entity discovery for a swimlane (the explicit 'try again' for a failed discovery). This starts background work and uses compute; do not call it repeatedly. Returns the swimlane with discovery moved to `initializing`.","operationId":"retry_discovery_endpoint_external_v1_swimlanes__swimlane_id__discovery_retry_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/live-summary/refresh":{"post":{"tags":["external-v1"],"summary":"Refresh Live Summary Endpoint","description":"Force regeneration of the API key user's live summary for a swimlane, bypassing the cache. This starts new AI generation and uses compute (latency and cost); do not call it repeatedly. It returns immediately with `state=generating`; re-read the matching read operation until `state` settles.","operationId":"refresh_live_summary_endpoint_external_v1_swimlanes__swimlane_id__live_summary_refresh_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneLiveSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/attention/refresh":{"post":{"tags":["external-v1"],"summary":"Refresh Attention Endpoint","description":"Force regeneration of the API key user's attention items for a swimlane. This starts new AI generation and uses compute (latency and cost); do not call it repeatedly. It returns immediately with `state=generating`; re-read the matching read operation until `state` settles.","operationId":"refresh_attention_endpoint_external_v1_swimlanes__swimlane_id__attention_refresh_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneAttentionItems"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/whats-next/refresh":{"post":{"tags":["external-v1"],"summary":"Refresh Whats Next Endpoint","description":"Force regeneration of the API key user's forward-looking items for a swimlane. This starts new AI generation and uses compute (latency and cost); do not call it repeatedly. It returns immediately with `state=generating`; re-read the matching read operation until `state` settles.","operationId":"refresh_whats_next_endpoint_external_v1_swimlanes__swimlane_id__whats_next_refresh_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}},{"name":"viewer_now","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"The caller's current local time as an RFC 3339 timestamp with offset (e.g. 2026-01-02T09:00:00-08:00). Pass it on a refresh so relative time phrases are correct for you; when omitted the server uses its own clock.","title":"Viewer Now"},"description":"The caller's current local time as an RFC 3339 timestamp with offset (e.g. 2026-01-02T09:00:00-08:00). Pass it on a refresh so relative time phrases are correct for you; when omitted the server uses its own clock."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneWhatsNextItems"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/activity/refresh":{"post":{"tags":["external-v1"],"summary":"Refresh Activity Endpoint","description":"Force regeneration of the API key user's Event Feed for a swimlane. This starts new AI generation and uses compute (latency and cost); do not call it repeatedly. It returns immediately with `state=generating`; re-read the matching read operation until `state` settles.","operationId":"refresh_activity_endpoint_external_v1_swimlanes__swimlane_id__activity_refresh_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}},{"name":"viewer_now","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"The caller's current local time as an RFC 3339 timestamp with offset (e.g. 2026-01-02T09:00:00-08:00). Pass it on a refresh so relative time phrases are correct for you; when omitted the server uses its own clock.","title":"Viewer Now"},"description":"The caller's current local time as an RFC 3339 timestamp with offset (e.g. 2026-01-02T09:00:00-08:00). Pass it on a refresh so relative time phrases are correct for you; when omitted the server uses its own clock."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneActivityPage"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/attention/{item_id}":{"delete":{"tags":["external-v1"],"summary":"Dismiss Attention Endpoint","description":"Dismiss one attention item for the API key user (permanent, per-viewer). The `item_id` is valid only for the generation it was read from — it rotates on every regeneration — so read attention items immediately before dismissing. The response reports `dismissed`: true when a matching active item was dismissed, false when the id matched nothing (unknown or already-settled).","operationId":"dismiss_attention_endpoint_external_v1_swimlanes__swimlane_id__attention__item_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}},{"name":"item_id","in":"path","required":true,"schema":{"type":"string","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneAck"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/external/v1/swimlanes/{swimlane_id}/attention/{item_id}/resolve":{"post":{"tags":["external-v1"],"summary":"Resolve Attention Endpoint","description":"Resolve one active attention item for the API key user (permanent, per-viewer) — the viewer has acted on it and closed it out. Any active item can be resolved; a risk or contradiction the viewer has handled is the primary case. The `item_id` is valid only for the generation it was read from — it rotates on every regeneration — so read attention items immediately before resolving. The response reports `resolved`: true when a matching active item was resolved, false when the id matched nothing (unknown or already-settled).","operationId":"resolve_attention_endpoint_external_v1_swimlanes__swimlane_id__attention__item_id__resolve_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"swimlane_id","in":"path","required":true,"schema":{"type":"string","title":"Swimlane Id"}},{"name":"item_id","in":"path","required":true,"schema":{"type":"string","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSwimlaneAck"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AttentionEvidence":{"properties":{"why_it_matters":{"type":"string","title":"Why It Matters","default":""},"primary_evidence":{"anyOf":[{"$ref":"#/components/schemas/AttentionEvidenceCitation"},{"type":"null"}]},"what_led_to_this":{"items":{"$ref":"#/components/schemas/AttentionEvidenceEvent"},"type":"array","title":"What Led To This","default":[]},"suggested_next_step":{"type":"string","title":"Suggested Next Step","default":""},"conflict_side_a":{"anyOf":[{"$ref":"#/components/schemas/AttentionEvidenceCitation"},{"type":"null"}]},"conflict_side_b":{"anyOf":[{"$ref":"#/components/schemas/AttentionEvidenceCitation"},{"type":"null"}]},"decision_at_stake":{"type":"string","title":"Decision At Stake","default":""}},"type":"object","title":"AttentionEvidence","description":"Expanded-view payload for a typed attention item (risk/contradiction).\n\nOptional on the wire — a general (``other``) item carries none, and old\nclients ignore it. ``primary_evidence`` is the single most load-bearing\ncitation; ``what_led_to_this`` is the ordered event trail; ``conflict``\n(contradiction only) carries the two disagreeing sides side-by-side. Every\nfield defaults so a partial payload never breaks rendering.\n\n@xref backend/models/swimlanes.py (SwimlaneAttentionItem.evidence) — the JSONB column this serializes to."},"AttentionEvidenceCitation":{"properties":{"quote":{"type":"string","title":"Quote","default":""},"source":{"type":"string","title":"Source","default":""},"date":{"type":"string","title":"Date","default":""},"key":{"type":"string","title":"Key","default":""},"source_type":{"type":"string","title":"Source Type","default":""},"title":{"type":"string","title":"Title","default":""},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timestamp"}},"type":"object","title":"AttentionEvidenceCitation","description":"One side of the load-bearing evidence: a verbatim quote + its provenance.\n\n``quote``/``source``/``date`` are the model-emitted provenance (``source`` a\nraw source-type token, ``date`` the fact's UTC calendar day). When the\nmodel's copied ``«type:id»`` token resolves in the corpus citation registry,\nthe resolved-citation fields (``key``/``source_type``/``title``/``url``/\n``timestamp``) are also filled so the card renders a clickable\n``CitationChip`` with a brand logo and a working link — the same grounding\nthe Event Feed uses. They are empty/``None`` when the token did not resolve\n(an older generation, or a quote the model could not anchor), and the card\nfalls back to the plain ``date · source`` stamp.\n\nAdditive and optional so older clients ignore the resolved fields.\n\n@xref backend/services/swimlanes/attention_risks.py (_to_evidence) — resolves the token into these fields.\n@xref packages/shared/src/features/swimlanes/components/swimlane-attention-evidence.tsx — reads key/url/source_type to render the chip; change both together."},"AttentionEvidenceEvent":{"properties":{"date":{"type":"string","title":"Date","default":""},"source":{"type":"string","title":"Source","default":""},"summary":{"type":"string","title":"Summary","default":""},"quote":{"type":"string","title":"Quote","default":""},"key":{"type":"string","title":"Key","default":""},"source_type":{"type":"string","title":"Source Type","default":""},"title":{"type":"string","title":"Title","default":""},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timestamp"}},"type":"object","title":"AttentionEvidenceEvent","description":"One event on a typed item's ``what_led_to_this`` trail.\n\nNewest-first ordered by the finder. ``date`` is the fact's UTC calendar day\n(``YYYY-MM-DD``) or empty for an undated (upload-shaped) fact; ``source`` is\nthe source-type discriminant; ``quote`` is an optional verbatim snippet.\n\nThe resolved-citation fields (``key``/``source_type``/``title``/``url``/\n``timestamp``) are filled when the model's copied ``«type:id»`` token\nresolves in the corpus registry, so a trail row renders the same clickable\n``CitationChip`` the primary/conflict citations do; empty otherwise (the row\nfalls back to the ``date · source`` stamp). Additive and optional so older\nclients ignore them.\n\n@xref backend/services/swimlanes/attention_risks.py (_to_evidence) — resolves the token into these fields; change both together.\n@xref packages/shared/src/features/swimlanes/components/swimlane-attention-evidence.tsx (toResolvedCitation) — reads key/url/source_type to render the chip; change both together."},"BasecampRecordingSourcePayload":{"properties":{"kind":{"type":"string","const":"basecamp_recording","title":"Kind","default":"basecamp_recording"},"recording_id":{"type":"string","title":"Recording Id"},"recording_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Type"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"project_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Id"},"project_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Name"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["recording_id"],"title":"BasecampRecordingSourcePayload","description":"Source metadata for a chunk derived from a Basecamp recording.\n\n``recording_id`` is the Basecamp recording's own id, which is unique within\nan account — not a composite ingest key, so it is safe to render.\n``recording_type`` is what a client filters on: all four ingested types\n(message, to-do, document, comment) share one source type, so the\ndistinction lives here.\n\n``project_id`` is the visibility key. It is exposed because a client that\ngroups by project needs it, and it is not sensitive on its own: holding the\nid grants nothing, since every read is still filtered by the caller's\nmaterialised grants.\n\nThis payload carries no time-tracking, hill-chart or gauge data; those are\nnot ingested at all.\n\n@xref backend/api/entity_memory.py (BasecampRecordingSourcePayload) — this external payload is a deliberate subset of the internal API shape; changes to the internal payload must keep this in sync\n@xref backend/services/basecamp_recording_render.py (render_basecamp_recording) — the field rule this describes; update both together."},"ChatUpdateSourcePayload":{"properties":{"kind":{"type":"string","const":"chat_update","title":"Kind","default":"chat_update"}},"type":"object","title":"ChatUpdateSourcePayload","description":"Source metadata for a chat-authored, org-visible project update.\n\nMirrors :class:`ManualSourcePayload` on the external API: no provider\nid is meaningful (the ``SourceEvent`` is synthetic), so the payload\ncarries only the discriminator.  ``created_by_user_id`` is\ndeliberately NOT propagated for the same PII / public-contract reason\nit is omitted from :class:`ManualSourcePayload` (internal\nautoincrement ids are not exposed on ``/external/v1`` or the MCP\nsurface — there is no public user-resolution endpoint).\n\nThe discriminator differs from ``manual`` because the two sources\nhave different ACLs (manual is owner-only, chat_update is\norg-visible); collapsing them onto one ``kind`` would let a client\nrender an org-shared update as a private note."},"ClickUpTaskSourcePayload":{"properties":{"kind":{"type":"string","const":"clickup_task","title":"Kind","default":"clickup_task"},"task_id":{"type":"string","title":"Task Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"space_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Space Id"},"list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Id"},"workspace_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace Id"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["task_id"],"title":"ClickUpTaskSourcePayload","description":"Source metadata for a chunk derived from a ClickUp task.\n\n``task_id`` IS the ClickUp task id (the connector's natural primary\nid, stored directly on ``SourceEvent.source_id``) — one field, one\nrole, mirroring :class:`LinearIssueSourcePayload.issue_id`.  The\ninternal ``is_mutable`` recency flag is intentionally NOT propagated:\nit's an in-app \"Live\" render hint, and the public payloads omit\nsuch internal flags for the same reason the Linear/GitHub/Slack\npayloads do."},"CoAttendanceExternalResponse":{"properties":{"kind":{"type":"string","const":"co_attendance","title":"Kind","default":"co_attendance"},"counterpart_entity_id":{"type":"string","title":"Counterpart Entity Id"},"co_event_count":{"type":"integer","title":"Co Event Count"},"first_event_time":{"type":"string","format":"date-time","title":"First Event Time"},"last_event_time":{"type":"string","format":"date-time","title":"Last Event Time"},"source_event_ids":{"items":{"type":"string"},"type":"array","title":"Source Event Ids"}},"type":"object","required":["counterpart_entity_id","co_event_count","first_event_time","last_event_time","source_event_ids"],"title":"CoAttendanceExternalResponse","description":"Aggregated co-presence row on the external API."},"CoAttendanceWindowExternalResponse":{"properties":{"since":{"type":"string","format":"date-time","title":"Since"},"until":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Until"}},"type":"object","required":["since"],"title":"CoAttendanceWindowExternalResponse","description":"Effective ``(since, until)`` window applied to co-attendance.\n\nEchoed regardless of whether the caller supplied the bounds or\naccepted the default ``DEFAULT_CO_ATTENDANCE_WINDOW_DAYS`` window —\nsurfacing the effective window is what lets an external client tell\napart \"no shared meetings\" from \"no shared meetings within the\nwindow I asked for\"."},"ConfluencePageSourcePayload":{"properties":{"kind":{"type":"string","const":"confluence_page","title":"Kind","default":"confluence_page"},"content_id":{"type":"string","title":"Content Id"},"content_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Type"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"space_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Space Key"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["content_id"],"title":"ConfluencePageSourcePayload","description":"Source metadata for a chunk derived from a Confluence page/blogpost.\n\n``content_id`` IS the Confluence content id (stored directly on\n``SourceEvent.source_id``); ``content_type`` distinguishes page vs blogpost.\nThe internal ``is_mutable`` recency flag is intentionally NOT propagated (an\nin-app \"Live\" render hint), matching every other external payload."},"CounterpartProjectionExternalResponse":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"canonical_name":{"type":"string","title":"Canonical Name"},"entity_type":{"type":"string","title":"Entity Type"}},"type":"object","required":["entity_id","canonical_name","entity_type"],"title":"CounterpartProjectionExternalResponse","description":"Canonical name + entity type for a counterpart referenced on the page.\n\nSame shape and contract as the in-app counterpart projection\n(see :class:`backend.api.entity_memory.CounterpartProjectionResponse`)\n— kept as a separate symbol so the external API can evolve its\nfield set independently of the in-app one, but identical\nsemantics today."},"CreateSwimlaneBody":{"properties":{"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"goal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal"},"owner_user_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Owner User Id","description":"Sentra user id — resolve names via GET /external/v1/org/members."},"stakeholder_user_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Stakeholder User Ids","description":"Sentra user ids — resolve names via GET /external/v1/org/members."}},"type":"object","required":["title","description"],"title":"CreateSwimlaneBody"},"ERPNextDocumentSourcePayload":{"properties":{"kind":{"type":"string","const":"erpnext_document","title":"Kind","default":"erpnext_document"},"source_ref":{"type":"string","title":"Source Ref"},"doctype":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doctype"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["source_ref"],"title":"ERPNextDocumentSourcePayload","description":"Source metadata for a chunk derived from an ERPNext document.\n\n``source_ref`` is the composite ``\"{doctype}:{name}\"`` stored on\n``SourceEvent.source_id`` — a Frappe ``name`` is unique only within its\ndoctype. ``doctype`` is what distinguishes a project from a customer: all\ningested doctypes share one source type, so the distinction lives here.\n\nThis payload carries no typed field values. Numeric fields on the record,\nmonetary amounts included, reach the model only inside the rendered text,\neach with an as-of date; a figure quoted inside a comment or linked email\nappears as prose.\n\n@xref backend/api/entity_memory.py (ERPNextDocumentSourcePayload) — this external payload is a deliberate subset of the internal API shape; changes to the internal payload must keep this in sync\n@xref backend/services/erpnext_document_render.py (render_erpnext_document) — the field rule this describes; update both together."},"EmailSourcePayload":{"properties":{"kind":{"type":"string","const":"email","title":"Kind","default":"email"},"thread_key":{"type":"string","title":"Thread Key"},"source_ref":{"type":"string","title":"Source Ref","default":""},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"sender_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Address"},"classification":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Classification"}},"type":"object","required":["thread_key"],"title":"EmailSourcePayload","description":"Source metadata for a chunk derived from an email thread.\n\n``thread_key`` is the RFC822 ``References``-chain root (mailbox-independent\nthread id); ``sender_address`` is the latest message's normalized sender.\n``source_ref`` is the ingest-level identity (``SourceEvent.source_id``): the\n``thread_key`` for normal threads, or ``\"msg:<id>\"`` for a one-off message\nwith no thread. It is always present and is the stable citation handle for\nsingle-message emails whose ``thread_key`` is empty.\nNo participant list is exposed — the recipient set is access-control\nmetadata, not a public deep-link field, and surfacing it would leak the\nthread's membership to any caller who can see one chunk."},"EmailThreadListItem":{"properties":{"thread_key":{"type":"string","title":"Thread Key","description":"Root of the RFC822 References chain identifying the thread. Pass this to ``GET /external/v1/emails/threads/raw`` to fetch the thread's raw message bodies."},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Subject of the latest message in the thread the caller can see."},"participants":{"items":{"type":"string"},"type":"array","title":"Participants","description":"Union of normalized participant addresses (sender + To + Cc) across the thread's messages that match the target AND are visible to the caller. Always includes the requested address."},"last_internal_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Internal Date","description":"Gmail ``internalDate`` of the latest message in the thread the caller can see. Threads are ordered by this field, descending."},"message_count":{"type":"integer","title":"Message Count","description":"Number of messages in this thread that match the target AND are visible to the caller. This is NOT necessarily the full thread length — messages the caller cannot see are not counted."},"has_attachments":{"type":"boolean","title":"Has Attachments","description":"True if any visible, target-matching message in the thread has attachments.","default":false},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary","description":"Extracted one-line summary of the latest visible message, when one has been computed; otherwise null."}},"type":"object","required":["thread_key","message_count"],"title":"EmailThreadListItem","description":"One thread the target participates in, as the caller can see it."},"EmailThreadListResponse":{"properties":{"email_address":{"type":"string","title":"Email Address","description":"Echo of the normalized target address the list was scoped to."},"thread_count":{"type":"integer","title":"Thread Count","description":"Number of threads on this page (already visibility-filtered)."},"threads":{"items":{"$ref":"#/components/schemas/EmailThreadListItem"},"type":"array","title":"Threads"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page; null on the final page. Pass it back unmodified as the ``cursor`` query parameter."}},"type":"object","required":["email_address","thread_count","threads"],"title":"EmailThreadListResponse","description":"Wire shape for ``GET /external/v1/emails/threads``."},"EntityChunksResponse":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"entity_type":{"type":"string","title":"Entity Type"},"entity_type_display_name":{"type":"string","title":"Entity Type Display Name"},"entity_name":{"type":"string","title":"Entity Name"},"is_admin":{"type":"boolean","title":"Is Admin"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"has_more":{"type":"boolean","title":"Has More"},"chunks":{"items":{"$ref":"#/components/schemas/FactChunkResponse"},"type":"array","title":"Chunks"},"has_relationships":{"type":"boolean","title":"Has Relationships","default":false}},"type":"object","required":["entity_id","entity_type","entity_type_display_name","entity_name","is_admin","limit","offset","has_more","chunks"],"title":"EntityChunksResponse"},"EntityListResponse":{"properties":{"org_id":{"type":"integer","title":"Org Id"},"total_entities":{"type":"integer","title":"Total Entities"},"groups":{"items":{"$ref":"#/components/schemas/EntityTypeGroup"},"type":"array","title":"Groups"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"total_is_lower_bound":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Total Is Lower Bound"}},"type":"object","required":["org_id","total_entities","groups"],"title":"EntityListResponse"},"EntityRelationshipsExternalResponse":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"entity_type":{"type":"string","title":"Entity Type"},"entity_type_display_name":{"type":"string","title":"Entity Type Display Name"},"entity_name":{"type":"string","title":"Entity Name"},"is_admin":{"type":"boolean","title":"Is Admin"},"co_attendance_window":{"$ref":"#/components/schemas/CoAttendanceWindowExternalResponse"},"links":{"items":{"$ref":"#/components/schemas/ExtractedLinkExternalResponse"},"type":"array","title":"Links"},"co_attendance":{"items":{"$ref":"#/components/schemas/CoAttendanceExternalResponse"},"type":"array","title":"Co Attendance"},"next_links_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Links Cursor"},"counterparts":{"additionalProperties":{"$ref":"#/components/schemas/CounterpartProjectionExternalResponse"},"type":"object","title":"Counterparts"}},"type":"object","required":["entity_id","entity_type","entity_type_display_name","entity_name","is_admin","co_attendance_window","links","co_attendance"],"title":"EntityRelationshipsExternalResponse"},"EntityStatus":{"type":"string","enum":["confirmed","proposed","rejected"],"title":"EntityStatus","description":"Curation state of an :class:`Entity`.\n\nStored as a plain ``VARCHAR`` (not a Postgres enum) for the same reason\n:class:`MemorySourceType` is — adding a future state stays a value-level\nchange instead of a forward-only enum migration.  ``CONFIRMED`` is the\ndefault for every entity created before this column existed and for every\nentity of an unlocked type; a *locked* entity type mints new entities as\n``PROPOSED`` until a human promotes them.  ``REJECTED`` is a curation\nverdict only — it never changes ingest behaviour (mentions still resolve\nonto a rejected entity so a later pass can enrich it, and a user can pull\nit back out of the rejected pile)."},"EntitySummary":{"properties":{"id":{"type":"string","title":"Id"},"entity_type":{"type":"string","title":"Entity Type"},"canonical_name":{"type":"string","title":"Canonical Name"},"status":{"$ref":"#/components/schemas/EntityStatus","default":"confirmed"},"match_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match Type"},"matched_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matched Text"}},"type":"object","required":["id","entity_type","canonical_name"],"title":"EntitySummary"},"EntityTypeGroup":{"properties":{"entity_type":{"type":"string","title":"Entity Type"},"display_name":{"type":"string","title":"Display Name"},"archived":{"type":"boolean","title":"Archived"},"locked":{"type":"boolean","title":"Locked","default":false},"description":{"type":"string","title":"Description","default":""},"count":{"type":"integer","title":"Count"},"entities":{"items":{"$ref":"#/components/schemas/EntitySummary"},"type":"array","title":"Entities"}},"type":"object","required":["entity_type","display_name","archived","count","entities"],"title":"EntityTypeGroup"},"EntityTypeListItem":{"properties":{"slug":{"type":"string","title":"Slug"},"display_name":{"type":"string","title":"Display Name"},"description":{"type":"string","title":"Description","default":""},"archived":{"type":"boolean","title":"Archived"}},"type":"object","required":["slug","display_name","archived"],"title":"EntityTypeListItem"},"EntityTypeListResponse":{"properties":{"org_id":{"type":"integer","title":"Org Id"},"types":{"items":{"$ref":"#/components/schemas/EntityTypeListItem"},"type":"array","title":"Types"}},"type":"object","required":["org_id","types"],"title":"EntityTypeListResponse"},"ExternalSwimlaneAck":{"properties":{"ack":{"type":"boolean","title":"Ack","default":true},"dismissed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dismissed","description":"For dismiss: whether a matching active attention item was dismissed (false = the id matched nothing). Null for other operations."},"resolved":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Resolved","description":"For resolve: whether a matching active attention item was resolved (false = the id matched nothing). Null for other operations."}},"type":"object","title":"ExternalSwimlaneAck","description":"A simple ack for delete / dismiss / resolve.\n\n``dismissed`` is populated only by the dismiss operation: ``true`` when a\nmatching active item was dismissed, ``false`` when the id matched nothing\n(unknown, already-dismissed, or superseded). It is ``null`` for delete.\n\n``resolved`` is populated only by the resolve operation, with the same\n``true``/``false``/``null`` semantics against the resolve action."},"ExternalSwimlaneActivityEvent":{"properties":{"source_type":{"type":"string","title":"Source Type"},"source_id":{"type":"string","title":"Source Id"},"title":{"type":"string","title":"Title"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"timestamp":{"type":"string","title":"Timestamp"},"preview":{"type":"string","title":"Preview"},"subtitle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subtitle"},"extra":{"additionalProperties":true,"type":"object","title":"Extra","default":{}},"type":{"anyOf":[{"$ref":"#/components/schemas/SwimlaneEventType"},{"type":"null"}]},"confidence":{"anyOf":[{"$ref":"#/components/schemas/SwimlaneEventConfidence"},{"type":"null"}]},"event_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Time"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"evidence":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Evidence","default":[]},"status":{"anyOf":[{"$ref":"#/components/schemas/SwimlaneEventStatus"},{"type":"null"}]},"id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Id","description":"swimlane_events.id when the card is backed by a persisted Event Feed row; omitted on synthesized legacy cards. Not stable across feed refresh — regeneration supersedes prior rows and new cards get new ids."}},"type":"object","required":["source_type","source_id","title","timestamp","preview"],"title":"ExternalSwimlaneActivityEvent","description":"External mirror of :class:`SwimlaneActivityEvent` including row ``id``."},"ExternalSwimlaneActivityPage":{"properties":{"state":{"type":"string","enum":["generating","ready","failed"],"title":"State","description":"Generation lifecycle state for this content type. The state enums are not unified across content types — read the per-operation docs. A run that stalled while generating reads as `failed` so the state agrees with `is_stale`/`is_generating`; recover it with the matching `refresh_*`."},"is_generating":{"type":"boolean","title":"Is Generating","description":"True while any run for you is in flight (a refresh you triggered, a run started in-app, or a concurrent refresh) — not only a refresh from this caller. When true, re-read until it is false rather than treating the current result as final."},"is_stale":{"type":"boolean","title":"Is Stale","description":"True when a `refresh` is warranted: either the cached result is terminal but its TTL has lapsed, or a refresh you triggered has stalled (it stopped advancing without settling). Orthogonal to `state`: a successful result can be stale. When true, call the matching `refresh_*` operation."},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At","description":"RFC 3339 UTC timestamp of the cached result, or null before the first generation."},"retry_after_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Retry After Seconds","description":"Seconds to wait before re-reading while a refresh is in flight (the MCP equivalent of the HTTP `Retry-After` header). Null when the result is settled — do not poll a settled read."},"events":{"items":{"$ref":"#/components/schemas/ExternalSwimlaneActivityEvent"},"type":"array","title":"Events","default":[]},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"degraded":{"type":"boolean","title":"Degraded","default":false}},"type":"object","required":["state","is_generating","is_stale"],"title":"ExternalSwimlaneActivityPage","description":"External mirror of :class:`SwimlaneActivityPage` with freshness (D3).\n\nCarries the same freshness envelope as the sibling generated reads so an\nexternal caller can tell an empty-because-not-yet-generated feed apart from\nan empty-because-quiet project and drive its own regeneration. ``degraded``\ncontinues to ride the page as a partial-corpus signal, orthogonal to the\nfreshness fields."},"ExternalSwimlaneAttentionItem":{"properties":{"id":{"type":"string","title":"Id"},"urgency":{"type":"string","enum":["low","medium","high","critical"],"title":"Urgency"},"type":{"type":"string","enum":["risk","contradiction","other"],"title":"Type","description":"What the item is: a detected `risk`, a `contradiction`, or a general assessment (`other`). Typed items carry an `evidence` payload and a `confidence` score; `other` items carry neither.","default":"other"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"evidence":{"anyOf":[{"$ref":"#/components/schemas/AttentionEvidence"},{"type":"null"}],"description":"Expanded assessment for a typed item (risk / contradiction): why it matters, the primary quote, the event trail, a suggested next step, and — for a contradiction — the two conflicting sides. Absent for a general (`other`) item and for older clients."},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence","description":"For a typed risk, the detection ensemble's vote share (0-1): the fraction of sub-runs that surfaced this risk. It is a detection agreement signal, NOT the probability the risk is real. Absent for the general pass and for older clients."}},"type":"object","required":["id","urgency","title","description"],"title":"ExternalSwimlaneAttentionItem","description":"External mirror of :class:`SwimlaneAttentionItem` (identical fields)."},"ExternalSwimlaneAttentionItems":{"properties":{"state":{"type":"string","enum":["generating","ready","failed"],"title":"State","description":"Generation lifecycle state for this content type. The state enums are not unified across content types — read the per-operation docs. A run that stalled while generating reads as `failed` so the state agrees with `is_stale`/`is_generating`; recover it with the matching `refresh_*`."},"is_generating":{"type":"boolean","title":"Is Generating","description":"True while any run for you is in flight (a refresh you triggered, a run started in-app, or a concurrent refresh) — not only a refresh from this caller. When true, re-read until it is false rather than treating the current result as final."},"is_stale":{"type":"boolean","title":"Is Stale","description":"True when a `refresh` is warranted: either the cached result is terminal but its TTL has lapsed, or a refresh you triggered has stalled (it stopped advancing without settling). Orthogonal to `state`: a successful result can be stale. When true, call the matching `refresh_*` operation."},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At","description":"RFC 3339 UTC timestamp of the cached result, or null before the first generation."},"retry_after_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Retry After Seconds","description":"Seconds to wait before re-reading while a refresh is in flight (the MCP equivalent of the HTTP `Retry-After` header). Null when the result is settled — do not poll a settled read."},"items":{"items":{"$ref":"#/components/schemas/ExternalSwimlaneAttentionItem"},"type":"array","title":"Items","default":[]}},"type":"object","required":["state","is_generating","is_stale"],"title":"ExternalSwimlaneAttentionItems","description":"The API key user's active attention items for a swimlane, with freshness."},"ExternalSwimlaneDetail":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"goal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal"},"owner":{"anyOf":[{"$ref":"#/components/schemas/ExternalSwimlanePerson"},{"type":"null"}]},"stakeholders":{"items":{"$ref":"#/components/schemas/ExternalSwimlanePerson"},"type":"array","title":"Stakeholders","default":[]},"parent_swimlane_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Swimlane Id"},"sub_swimlane_count":{"type":"integer","title":"Sub Swimlane Count"},"discovery_state":{"type":"string","enum":["initializing","ready","failed"],"title":"Discovery State"},"discovery_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discovery Error"},"discovered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discovered At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","title","sub_swimlane_count","discovery_state"],"title":"ExternalSwimlaneDetail","description":"External mirror of :class:`SwimlaneDetail` (a swimlane's own attributes)."},"ExternalSwimlaneInlineLiveSummary":{"properties":{"state":{"type":"string","enum":["generating","fresh","stale","empty","failed"],"title":"State"},"is_stale":{"type":"boolean","title":"Is Stale","description":"True when this cached summary warrants a `refresh_swimlane_live_summary`. Same rule as the dedicated get: a terminal result past its TTL, a failed generation, or a stalled refresh. Read this rather than computing staleness from `stale_after`.","default":false},"is_generating":{"type":"boolean","title":"Is Generating","description":"True while a refresh of this summary is in flight; re-read the swimlane rather than treating the current text as final.","default":false},"live_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Summary"},"citations":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Citations"},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At"},"stale_after":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stale After"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["state"],"title":"ExternalSwimlaneInlineLiveSummary","description":"External mirror of the inline live summary embedded in the list.\n\nCarries the internal ``SwimlaneLiveSummary`` fields plus the D3 freshness\nhints (``is_stale`` / ``is_generating``) so a caller that only lists learns\na summary is stale or mid-refresh without a per-swimlane get. ``stale_after``\nis kept for the UI/back-compat; agents should read ``is_stale``."},"ExternalSwimlaneList":{"properties":{"viewer":{"$ref":"#/components/schemas/ExternalSwimlanePerson"},"swimlanes":{"items":{"$ref":"#/components/schemas/ExternalSwimlaneWithLiveSummary"},"type":"array","title":"Swimlanes","default":[]}},"type":"object","required":["viewer"],"title":"ExternalSwimlaneList","description":"External mirror of :class:`SwimlaneList`."},"ExternalSwimlaneLiveSummary":{"properties":{"state":{"type":"string","enum":["generating","fresh","stale","empty","failed"],"title":"State","description":"Generation lifecycle state for this content type. The state enums are not unified across content types — read the per-operation docs. A run that stalled while generating reads as `failed` so the state agrees with `is_stale`/`is_generating`; recover it with the matching `refresh_*`."},"is_generating":{"type":"boolean","title":"Is Generating","description":"True while any run for you is in flight (a refresh you triggered, a run started in-app, or a concurrent refresh) — not only a refresh from this caller. When true, re-read until it is false rather than treating the current result as final."},"is_stale":{"type":"boolean","title":"Is Stale","description":"True when a `refresh` is warranted: either the cached result is terminal but its TTL has lapsed, or a refresh you triggered has stalled (it stopped advancing without settling). Orthogonal to `state`: a successful result can be stale. When true, call the matching `refresh_*` operation."},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At","description":"RFC 3339 UTC timestamp of the cached result, or null before the first generation."},"retry_after_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Retry After Seconds","description":"Seconds to wait before re-reading while a refresh is in flight (the MCP equivalent of the HTTP `Retry-After` header). Null when the result is settled — do not poll a settled read."},"live_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Live Summary"},"citations":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Citations"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["state","is_generating","is_stale"],"title":"ExternalSwimlaneLiveSummary","description":"The API key user's live summary for a swimlane, with freshness (D3)."},"ExternalSwimlanePerson":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"initials":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Initials"},"kind":{"type":"string","enum":["user","unresolved"],"title":"Kind","default":"user"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"is_pending_invite":{"type":"boolean","title":"Is Pending Invite","default":false}},"type":"object","required":["id","name"],"title":"ExternalSwimlanePerson","description":"External mirror of :class:`SwimlanePerson` (identical fields)."},"ExternalSwimlaneWhatsNextItem":{"properties":{"id":{"type":"string","title":"Id"},"type":{"type":"string","enum":["meeting","commitment","milestone","review","release"],"title":"Type"},"time_label":{"type":"string","title":"Time Label"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"people":{"items":{"$ref":"#/components/schemas/ExternalSwimlanePerson"},"type":"array","title":"People","default":[]}},"type":"object","required":["id","type","time_label","title","description"],"title":"ExternalSwimlaneWhatsNextItem","description":"External mirror of :class:`SwimlaneWhatsNextItem`.\n\nRe-points ``people`` at :class:`ExternalSwimlanePerson`; all other fields\nmatch the internal shape."},"ExternalSwimlaneWhatsNextItems":{"properties":{"state":{"type":"string","enum":["generating","ready","failed"],"title":"State","description":"Generation lifecycle state for this content type. The state enums are not unified across content types — read the per-operation docs. A run that stalled while generating reads as `failed` so the state agrees with `is_stale`/`is_generating`; recover it with the matching `refresh_*`."},"is_generating":{"type":"boolean","title":"Is Generating","description":"True while any run for you is in flight (a refresh you triggered, a run started in-app, or a concurrent refresh) — not only a refresh from this caller. When true, re-read until it is false rather than treating the current result as final."},"is_stale":{"type":"boolean","title":"Is Stale","description":"True when a `refresh` is warranted: either the cached result is terminal but its TTL has lapsed, or a refresh you triggered has stalled (it stopped advancing without settling). Orthogonal to `state`: a successful result can be stale. When true, call the matching `refresh_*` operation."},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At","description":"RFC 3339 UTC timestamp of the cached result, or null before the first generation."},"retry_after_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Retry After Seconds","description":"Seconds to wait before re-reading while a refresh is in flight (the MCP equivalent of the HTTP `Retry-After` header). Null when the result is settled — do not poll a settled read."},"items":{"items":{"$ref":"#/components/schemas/ExternalSwimlaneWhatsNextItem"},"type":"array","title":"Items","default":[]}},"type":"object","required":["state","is_generating","is_stale"],"title":"ExternalSwimlaneWhatsNextItems","description":"The API key user's forward-looking items for a swimlane, with freshness."},"ExternalSwimlaneWithLiveSummary":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"goal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal"},"owner":{"anyOf":[{"$ref":"#/components/schemas/ExternalSwimlanePerson"},{"type":"null"}]},"stakeholders":{"items":{"$ref":"#/components/schemas/ExternalSwimlanePerson"},"type":"array","title":"Stakeholders","default":[]},"parent_swimlane_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Swimlane Id"},"sub_swimlane_count":{"type":"integer","title":"Sub Swimlane Count"},"discovery_state":{"type":"string","enum":["initializing","ready","failed"],"title":"Discovery State"},"discovery_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discovery Error"},"discovered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discovered At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"live_summary":{"anyOf":[{"$ref":"#/components/schemas/ExternalSwimlaneInlineLiveSummary"},{"type":"null"}]}},"type":"object","required":["id","title","sub_swimlane_count","discovery_state"],"title":"ExternalSwimlaneWithLiveSummary","description":"External mirror of :class:`SwimlaneWithLiveSummary` (detail + cached summary)."},"ExtractedLinkExternalResponse":{"properties":{"kind":{"type":"string","const":"extracted_link","title":"Kind","default":"extracted_link"},"link_id":{"type":"string","title":"Link Id"},"counterpart_entity_id":{"type":"string","title":"Counterpart Entity Id"},"direction":{"type":"string","enum":["outgoing","incoming"],"title":"Direction"},"link_type":{"type":"string","title":"Link Type"},"link_type_display_name":{"type":"string","title":"Link Type Display Name"},"evidence_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Evidence Text"},"event_time":{"type":"string","format":"date-time","title":"Event Time"},"source_event_id":{"type":"string","title":"Source Event Id"},"source":{"oneOf":[{"$ref":"#/components/schemas/MeetingSourcePayload"},{"$ref":"#/components/schemas/FileSourcePayload"},{"$ref":"#/components/schemas/ManualSourcePayload"},{"$ref":"#/components/schemas/ChatUpdateSourcePayload"},{"$ref":"#/components/schemas/EmailSourcePayload"},{"$ref":"#/components/schemas/PersonalEmailSourcePayload"},{"$ref":"#/components/schemas/LinearIssueSourcePayload"},{"$ref":"#/components/schemas/ClickUpTaskSourcePayload"},{"$ref":"#/components/schemas/JiraIssueSourcePayload"},{"$ref":"#/components/schemas/ConfluencePageSourcePayload"},{"$ref":"#/components/schemas/HubSpotRecordSourcePayload"},{"$ref":"#/components/schemas/ERPNextDocumentSourcePayload"},{"$ref":"#/components/schemas/BasecampRecordingSourcePayload"},{"$ref":"#/components/schemas/ZendeskTicketSourcePayload"},{"$ref":"#/components/schemas/GitHubItemSourcePayload"},{"$ref":"#/components/schemas/SlackChannelDaySourcePayload"},{"$ref":"#/components/schemas/GoogleChatSpaceDaySourcePayload"},{"$ref":"#/components/schemas/TeamsChatDaySourcePayload"},{"$ref":"#/components/schemas/TeamsChannelDaySourcePayload"}],"title":"Source","discriminator":{"propertyName":"kind","mapping":{"basecamp_recording":"#/components/schemas/BasecampRecordingSourcePayload","chat_update":"#/components/schemas/ChatUpdateSourcePayload","clickup_task":"#/components/schemas/ClickUpTaskSourcePayload","confluence_page":"#/components/schemas/ConfluencePageSourcePayload","email":"#/components/schemas/EmailSourcePayload","erpnext_document":"#/components/schemas/ERPNextDocumentSourcePayload","github_item":"#/components/schemas/GitHubItemSourcePayload","google_chat_space_day":"#/components/schemas/GoogleChatSpaceDaySourcePayload","hubspot_company":"#/components/schemas/HubSpotRecordSourcePayload","hubspot_contact":"#/components/schemas/HubSpotRecordSourcePayload","hubspot_deal":"#/components/schemas/HubSpotRecordSourcePayload","hubspot_record":"#/components/schemas/HubSpotRecordSourcePayload","jira_issue":"#/components/schemas/JiraIssueSourcePayload","linear_issue":"#/components/schemas/LinearIssueSourcePayload","manual":"#/components/schemas/ManualSourcePayload","meeting":"#/components/schemas/MeetingSourcePayload","personal_email":"#/components/schemas/PersonalEmailSourcePayload","slack_channel_day":"#/components/schemas/SlackChannelDaySourcePayload","teams_channel_day":"#/components/schemas/TeamsChannelDaySourcePayload","teams_chat_day":"#/components/schemas/TeamsChatDaySourcePayload","user_file":"#/components/schemas/FileSourcePayload","zendesk_ticket":"#/components/schemas/ZendeskTicketSourcePayload"}}},"last_edited_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Edited At"}},"type":"object","required":["link_id","counterpart_entity_id","direction","link_type","link_type_display_name","event_time","source_event_id","source"],"title":"ExtractedLinkExternalResponse","description":"One LLM-extracted relationship row on the external API."},"FactChunkResponse":{"properties":{"chunk_id":{"type":"string","title":"Chunk Id"},"source_event_id":{"type":"string","title":"Source Event Id"},"fact_text":{"type":"string","title":"Fact Text"},"event_time":{"type":"string","format":"date-time","title":"Event Time"},"source":{"oneOf":[{"$ref":"#/components/schemas/MeetingSourcePayload"},{"$ref":"#/components/schemas/FileSourcePayload"},{"$ref":"#/components/schemas/ManualSourcePayload"},{"$ref":"#/components/schemas/ChatUpdateSourcePayload"},{"$ref":"#/components/schemas/EmailSourcePayload"},{"$ref":"#/components/schemas/PersonalEmailSourcePayload"},{"$ref":"#/components/schemas/LinearIssueSourcePayload"},{"$ref":"#/components/schemas/ClickUpTaskSourcePayload"},{"$ref":"#/components/schemas/JiraIssueSourcePayload"},{"$ref":"#/components/schemas/ConfluencePageSourcePayload"},{"$ref":"#/components/schemas/HubSpotRecordSourcePayload"},{"$ref":"#/components/schemas/ERPNextDocumentSourcePayload"},{"$ref":"#/components/schemas/BasecampRecordingSourcePayload"},{"$ref":"#/components/schemas/ZendeskTicketSourcePayload"},{"$ref":"#/components/schemas/GitHubItemSourcePayload"},{"$ref":"#/components/schemas/SlackChannelDaySourcePayload"},{"$ref":"#/components/schemas/GoogleChatSpaceDaySourcePayload"},{"$ref":"#/components/schemas/TeamsChatDaySourcePayload"},{"$ref":"#/components/schemas/TeamsChannelDaySourcePayload"}],"title":"Source","discriminator":{"propertyName":"kind","mapping":{"basecamp_recording":"#/components/schemas/BasecampRecordingSourcePayload","chat_update":"#/components/schemas/ChatUpdateSourcePayload","clickup_task":"#/components/schemas/ClickUpTaskSourcePayload","confluence_page":"#/components/schemas/ConfluencePageSourcePayload","email":"#/components/schemas/EmailSourcePayload","erpnext_document":"#/components/schemas/ERPNextDocumentSourcePayload","github_item":"#/components/schemas/GitHubItemSourcePayload","google_chat_space_day":"#/components/schemas/GoogleChatSpaceDaySourcePayload","hubspot_company":"#/components/schemas/HubSpotRecordSourcePayload","hubspot_contact":"#/components/schemas/HubSpotRecordSourcePayload","hubspot_deal":"#/components/schemas/HubSpotRecordSourcePayload","hubspot_record":"#/components/schemas/HubSpotRecordSourcePayload","jira_issue":"#/components/schemas/JiraIssueSourcePayload","linear_issue":"#/components/schemas/LinearIssueSourcePayload","manual":"#/components/schemas/ManualSourcePayload","meeting":"#/components/schemas/MeetingSourcePayload","personal_email":"#/components/schemas/PersonalEmailSourcePayload","slack_channel_day":"#/components/schemas/SlackChannelDaySourcePayload","teams_channel_day":"#/components/schemas/TeamsChannelDaySourcePayload","teams_chat_day":"#/components/schemas/TeamsChatDaySourcePayload","user_file":"#/components/schemas/FileSourcePayload","zendesk_ticket":"#/components/schemas/ZendeskTicketSourcePayload"}}},"last_edited_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Edited At"}},"type":"object","required":["chunk_id","source_event_id","fact_text","event_time","source"],"title":"FactChunkResponse"},"FileSourcePayload":{"properties":{"kind":{"type":"string","const":"user_file","title":"Kind","default":"user_file"},"file_id":{"type":"string","title":"File Id"},"original_filename":{"type":"string","title":"Original Filename"},"storage_kind":{"type":"string","title":"Storage Kind"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"local_machine_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Local Machine Id"},"provenance":{"anyOf":[{"$ref":"#/components/schemas/Provenance"},{"type":"null"}]},"owner_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Domain"},"attribution":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attribution"}},"type":"object","required":["file_id","original_filename","storage_kind"],"title":"FileSourcePayload"},"FollowupBurdenCohort":{"properties":{"targets":{"type":"integer","title":"Targets","description":"Visible targets in this snapshot set."},"total_asks":{"type":"integer","title":"Total Asks"},"total_chased_asks":{"type":"integer","title":"Total Chased Asks","description":"Sum of asks with >=1 confirmed inbound chase."},"total_inbound_chases_confirmed":{"type":"integer","title":"Total Inbound Chases Confirmed"},"total_inbound_chases_flagged":{"type":"integer","title":"Total Inbound Chases Flagged"},"total_unanswered":{"type":"integer","title":"Total Unanswered"},"total_responded":{"type":"integer","title":"Total Responded"},"confirm_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confirm Rate","description":"Confirmed / flagged inbound chases, 0-1. The share of deterministically-flagged chases the LLM verifier confirmed. ``null`` when no chases were flagged."},"chased_ask_ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Chased Ask Ratio","description":"Chased asks / total asks, 0-1. Roster-size-independent measure of how much of the cohort's inbound ask volume gets chased. ``null`` when there are no asks."},"pct_unanswered":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pct Unanswered","description":"Unanswered asks / total asks, 0-1. ``null`` when there are no asks."},"scored_targets":{"type":"integer","title":"Scored Targets","description":"Targets carrying a blocker score in this set."},"mean_blocker_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Mean Blocker Score","description":"Mean row-level blocker score across scored targets. ``null`` when no target is scored (e.g. a week before scoring shipped)."},"max_blocker_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Blocker Score","description":"Highest row-level blocker score in the set. ``null`` when unscored."}},"type":"object","required":["targets","total_asks","total_chased_asks","total_inbound_chases_confirmed","total_inbound_chases_flagged","total_unanswered","total_responded","scored_targets"],"title":"FollowupBurdenCohort","description":"Aggregate metrics across the caller's visible targets for one snapshot set.\n\nDerived on read by summing over exactly the ACL-filtered\n:class:`FollowupBurdenSummaryItem` list — never a raw table aggregate — so a\nscoped key gets the cohort of *its* visible targets and an org-wide (ALL)\nkey gets the true org cohort. When an ``as_of`` date narrows the summary to\na past week, this reflects that week.\n\nRatios are ``None`` when their denominator is zero (no asks / no flagged\nchases) so a consumer can tell \"undefined\" from a real zero."},"FollowupBurdenEvidenceAsk":{"properties":{"thread_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thread Key","description":"Pass to GET /external/v1/emails/threads/raw?thread_key=... to fetch the thread content (that endpoint enforces per-message email visibility)."},"subject":{"type":"string","title":"Subject"},"sender":{"type":"string","title":"Sender"},"ask_msg_id":{"type":"integer","title":"Ask Msg Id"},"ask_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ask Date"},"responded":{"type":"boolean","title":"Responded"},"latency_seconds":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latency Seconds"},"chase_msg_ids":{"items":{"type":"integer"},"type":"array","title":"Chase Msg Ids"},"chase_senders":{"items":{"anyOf":[{"type":"string"},{"type":"null"}]},"type":"array","title":"Chase Senders","description":"Normalized sender address (lowercased) of each chase message, parallel to ``chase_msg_ids``. ``null`` when the message body was not hydrated at snapshot time. Empty on snapshots written before this field existed."},"chase_dates":{"items":{"anyOf":[{"type":"string"},{"type":"null"}]},"type":"array","title":"Chase Dates","description":"ISO 8601 timestamp of each chase message, parallel to ``chase_msg_ids``. ``null`` when the message body was not hydrated at snapshot time. Empty on snapshots written before this field existed."},"sole_to":{"type":"boolean","title":"Sole To","description":"True iff the target was the only ``To:`` recipient on the ask message. False on multi-recipient asks and on snapshots written before this field existed.","default":false},"thread_message_count":{"type":"integer","title":"Thread Message Count","description":"Total number of messages in the ask's thread as hydrated at snapshot time. Zero on snapshots written before this field existed.","default":0},"thread_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thread Summary","description":"Chronological concatenation of every non-empty per-message ``extracted_summary`` in the ask's thread, joined by ``\\n----\\n``. ``null`` when no message in the thread has a summary yet or the snapshot pre-dates this field."},"resolution_kind":{"type":"string","title":"Resolution Kind","description":"Post-chase resolution state as judged by the thread verifier: ``NO_RESPONSE`` / ``HOLDING_RESPONSE`` / ``RESOLVED_SUBSTANTIVE``. Falls back to the deterministic default (``RESOLVED_SUBSTANTIVE`` when the target replied, ``NO_RESPONSE`` otherwise) on snapshots written before the verifier ran.","default":"NO_RESPONSE"},"resolution_reason":{"type":"string","title":"Resolution Reason","description":"One-sentence paraphrase of what closed or is holding the ask, produced by the thread verifier for ``HOLDING_RESPONSE`` and ``RESOLVED_SUBSTANTIVE`` states. Empty string on ``NO_RESPONSE`` and on snapshots pre-dating the verifier.","default":""},"impact":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impact","description":"Business-impact score 1-10 from the thread verifier. ``null`` on unscored asks (verifier off / errored) or pre-scoring snapshots."},"urgency":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Urgency","description":"Urgency/deadlock score 1-10. ``null`` when unscored."},"scope":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Scope","description":"Scope/friction score 1-10. ``null`` when unscored."},"impact_reason":{"type":"string","title":"Impact Reason","description":"One-sentence justification for ``impact``.","default":""},"urgency_reason":{"type":"string","title":"Urgency Reason","description":"One-sentence justification for ``urgency``.","default":""},"scope_reason":{"type":"string","title":"Scope Reason","description":"One-sentence justification for ``scope``.","default":""},"days_unresolved":{"type":"integer","title":"Days Unresolved","description":"Whole days from the ask's last chase (or ask date) to the snapshot, feeding the aging factor. Zero on pre-scoring snapshots.","default":0},"blocker_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Blocker Score","description":"Composite blocker score for this ask: ``(impact*0.40 + urgency*0.35 + scope*0.25) * min(2.0, 1 + 0.05*days_unresolved)`` (aging fixed at 1.0 for resolved asks). ``null`` when unscored."}},"type":"object","required":["subject","sender","ask_msg_id","responded"],"title":"FollowupBurdenEvidenceAsk","description":"One ask the target was chased on (metadata only — no message bodies)."},"FollowupBurdenSummaryItem":{"properties":{"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"computed_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Computed Date","description":"ISO date of the snapshot this row reflects."},"window_days":{"type":"integer","title":"Window Days"},"asks":{"type":"integer","title":"Asks"},"chased_asks":{"type":"integer","title":"Chased Asks","description":"Asks with >=1 (LLM-confirmed) inbound chase."},"inbound_chases_confirmed":{"type":"integer","title":"Inbound Chases Confirmed"},"inbound_chases_flagged":{"type":"integer","title":"Inbound Chases Flagged","description":"Deterministically flagged inbound chases before LLM filtering. Equals confirmed when the LLM filter did not run."},"llm_filtered":{"type":"integer","title":"Llm Filtered"},"responded":{"type":"integer","title":"Responded"},"unanswered":{"type":"integer","title":"Unanswered"},"median_latency_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Median Latency Hours"},"evidence_thread_count":{"type":"integer","title":"Evidence Thread Count","description":"Number of distinct threads the target was chased in."},"blocker_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Blocker Score","description":"Highest blocker score across this target's chased asks for the snapshot day: ``(impact*0.40 + urgency*0.35 + scope*0.25) * aging``, components 1-10 from the thread verifier, aging = ``min(2.0, 1 + 0.05*days_unresolved)`` (1.0 for resolved asks). ``null`` on snapshots written before scoring shipped or when no ask was scored. Summary items are ordered by this, highest first."},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade","description":"Short-code grade from the org roster: one of ``CXO``, ``SVP``, ``VP``, ``AVP``, ``GM``, ``AGM``, ``SM``, ``M``, ``AM``, ``SE``, ``E``, ``INT``, ``TR``, ``CON``. ``null`` when the target is not in the roster or the config is unavailable."},"kmt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kmt","description":"Display name of this target's KMT (top-of-house) owner as it appears in the roster (e.g. ``Peyush Bansal``, ``Ramneek Khurana``). ``null`` when the target is not in the roster or the config is unavailable."}},"type":"object","required":["email","window_days","asks","chased_asks","inbound_chases_confirmed","inbound_chases_flagged","llm_filtered","responded","unanswered","evidence_thread_count"],"title":"FollowupBurdenSummaryItem","description":"One target's latest snapshot, summary view."},"FollowupBurdenSummaryResponse":{"properties":{"org_id":{"type":"integer","title":"Org Id"},"count":{"type":"integer","title":"Count","description":"Targets in ``items`` — i.e. the size of THIS page, not the cohort. Use ``total`` for the number of targets visible to the caller."},"total":{"type":"integer","title":"Total","description":"Targets visible to the caller across all pages. ``cohort`` is aggregated over this full set, not over ``items``."},"limit":{"type":"integer","title":"Limit","description":"Page size actually applied (clamped to 1..500)."},"offset":{"type":"integer","title":"Offset","description":"Targets skipped before this page."},"has_more":{"type":"boolean","title":"Has More","description":"True when ``offset + count < total``; fetch the next page with ``offset = offset + count``."},"as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of","description":"ISO date the caller requested via ``as_of``; ``null`` when omitted (today's latest). Each item is that target's newest snapshot on or before this date."},"cohort":{"$ref":"#/components/schemas/FollowupBurdenCohort","description":"Aggregate metrics over every target visible to the caller (post-ACL), NOT just the current page — so ratios stay comparable across pages and page size. Reflects the requested ``as_of`` week when set."},"items":{"items":{"$ref":"#/components/schemas/FollowupBurdenSummaryItem"},"type":"array","title":"Items"}},"type":"object","required":["org_id","count","total","limit","offset","has_more","cohort","items"],"title":"FollowupBurdenSummaryResponse","description":"Wire shape for ``GET /external/v1/followup-burden``."},"FollowupBurdenUserResponse":{"properties":{"org_id":{"type":"integer","title":"Org Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"computed_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Computed Date"},"window_days":{"type":"integer","title":"Window Days"},"asks":{"type":"integer","title":"Asks"},"chased_asks":{"type":"integer","title":"Chased Asks"},"inbound_chases_confirmed":{"type":"integer","title":"Inbound Chases Confirmed"},"inbound_chases_flagged":{"type":"integer","title":"Inbound Chases Flagged"},"llm_filtered":{"type":"integer","title":"Llm Filtered"},"outbound_chases":{"type":"integer","title":"Outbound Chases"},"responded":{"type":"integer","title":"Responded"},"unanswered":{"type":"integer","title":"Unanswered"},"median_latency_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Median Latency Hours"},"blocker_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Blocker Score","description":"Highest blocker score across this target's chased asks for the snapshot (same formula as the per-ask ``evidence[].blocker_score``). ``null`` on pre-scoring snapshots or when no ask was scored. Per-ask scores are in ``evidence``."},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade","description":"Short-code grade from the org roster: one of ``CXO``, ``SVP``, ``VP``, ``AVP``, ``GM``, ``AGM``, ``SM``, ``M``, ``AM``, ``SE``, ``E``, ``INT``, ``TR``, ``CON``. ``null`` when the target is not in the roster or the config is unavailable."},"kmt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kmt","description":"Display name of this target's KMT (top-of-house) owner as it appears in the roster. ``null`` when the target is not in the roster or the config is unavailable."},"sole_to_asks":{"type":"integer","title":"Sole To Asks"},"multi_to_asks":{"type":"integer","title":"Multi To Asks"},"llm_applied":{"type":"boolean","title":"Llm Applied"},"evidence_thread_keys":{"items":{"type":"string"},"type":"array","title":"Evidence Thread Keys"},"evidence":{"items":{"$ref":"#/components/schemas/FollowupBurdenEvidenceAsk"},"type":"array","title":"Evidence"}},"type":"object","required":["org_id","email","window_days","asks","chased_asks","inbound_chases_confirmed","inbound_chases_flagged","llm_filtered","outbound_chases","responded","unanswered","sole_to_asks","multi_to_asks","llm_applied"],"title":"FollowupBurdenUserResponse","description":"Wire shape for ``GET /external/v1/followup-burden/user``."},"GitHubItemSourcePayload":{"properties":{"kind":{"type":"string","const":"github_item","title":"Kind","default":"github_item"},"source_ref":{"type":"string","title":"Source Ref"},"item_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Item Type"},"repo_full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Repo Full Name"},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["source_ref"],"title":"GitHubItemSourcePayload","description":"Source metadata for a chunk derived from a GitHub PR or issue.\n\n``source_ref`` is the composite ``\"{item_type}:{repo_id}:{item_id}\"``\nstring stored on ``SourceEvent.source_id`` — an opaque ingest key,\nNOT the human-readable GitHub PR/issue number.  The provider's\nown numeric id is exposed separately as ``number`` so a client that\nwants the canonical \"PR #42\" label doesn't accidentally render the\ncomposite key."},"GoogleChatSpaceDaySourcePayload":{"properties":{"kind":{"type":"string","const":"google_chat_space_day","title":"Kind","default":"google_chat_space_day"},"space_id":{"type":"string","title":"Space Id"},"space_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Space Name"},"day":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Day"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["space_id"],"title":"GoogleChatSpaceDaySourcePayload","description":"Source metadata for a chunk derived from a Chat (space, day) bucket.\n\n@xref backend/api/entity_memory.py (GoogleChatSpaceDaySourcePayload) — this external payload is a deliberate subset of the internal API shape; changes to the internal payload must keep this in sync"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HubSpotRecordSourcePayload":{"properties":{"kind":{"type":"string","enum":["hubspot_contact","hubspot_company","hubspot_deal","hubspot_record"],"title":"Kind","default":"hubspot_record"},"record_id":{"type":"string","title":"Record Id"},"object_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Object Type"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"owner_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Id"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["record_id"],"title":"HubSpotRecordSourcePayload","description":"Source metadata for a chunk derived from a HubSpot CRM record.\n\n``record_id`` IS the HubSpot record id (stored on ``SourceEvent.source_id``).\n``kind`` distinguishes the three CRM object types. The internal\n``is_mutable`` recency flag is intentionally NOT propagated (in-app \"Live\"\nrender hint only), matching every other external payload; ``owner_id`` is\nthe HubSpot ownerId, retained for provenance, not a Sentra user id."},"ImportantEntitiesResponse":{"properties":{"org_id":{"type":"integer","title":"Org Id"},"count":{"type":"integer","title":"Count","description":"The number of returned items (at most `limit`), not a total. This route is a bounded top-N read with no pagination cursor."},"items":{"items":{"$ref":"#/components/schemas/ImportantEntity"},"type":"array","title":"Items"}},"type":"object","required":["org_id","count","items"],"title":"ImportantEntitiesResponse","description":"Bounded top-N read: there is no total and no pagination cursor,\nunlike the cursor-paginated ``GET /external/v1/entities``."},"ImportantEntity":{"properties":{"entity_id":{"type":"string","format":"uuid","title":"Entity Id"},"entity_type":{"type":"string","title":"Entity Type"},"name":{"type":"string","title":"Name"}},"type":"object","required":["entity_id","entity_type","name"],"title":"ImportantEntity","description":"One entity in a caller's importance set: identity only. Scores and\nsignal components stay internal — they are not part of the public\ncontract, so no surface returns them."},"JiraIssueSourcePayload":{"properties":{"kind":{"type":"string","const":"jira_issue","title":"Kind","default":"jira_issue"},"issue_id":{"type":"string","title":"Issue Id"},"issue_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Issue Key"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"project_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Key"},"project_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Name"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["issue_id"],"title":"JiraIssueSourcePayload","description":"Source metadata for a chunk derived from a Jira issue.\n\n``issue_id`` IS the Jira issue id (the connector's natural primary id,\nstored directly on ``SourceEvent.source_id``); ``issue_key`` is the\nhuman-readable key (e.g. ``ENG-42``) for display. The internal\n``is_mutable`` recency flag is intentionally NOT propagated: it's an in-app\n\"Live\" render hint, and the public payloads omit such internal flags for the\nsame reason the Linear/GitHub/Slack payloads do."},"LinearIssueSourcePayload":{"properties":{"kind":{"type":"string","const":"linear_issue","title":"Kind","default":"linear_issue"},"issue_id":{"type":"string","title":"Issue Id"},"project_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Name"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"team_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Team Id"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["issue_id"],"title":"LinearIssueSourcePayload","description":"Source metadata for a chunk derived from a Linear issue."},"ManualSourcePayload":{"properties":{"kind":{"type":"string","const":"manual","title":"Kind","default":"manual"}},"type":"object","title":"ManualSourcePayload","description":"Source metadata for a user-authored manual chunk on the external API.\n\nNo provider id is meaningful — manual entries have no underlying\nsource system to link back to, so the payload carries only the\ndiscriminator.\n\nNo creator attribution field by design: the in-app projector\nsurfaces ``created_by_user_id`` (an internal autoincrement int)\nfor the web UI, but the same field cannot ship on /external/v1\nor the MCP tool surface — internal user ids are enumerable PII-\nadjacent identifiers with no resolution endpoint on the public\nAPI (no ``/external/v1/users/{id}``), and the rest of\nexternal-v1 addresses users by string (e.g. ``attendee:\nOptional[str]`` on the meetings endpoints).  See commit\n``2e1253bec`` which removed ``last_edited_by_user_id`` from\n:class:`FactChunkExternalResponse` and\n:class:`ExtractedLinkExternalResponse` for the same reasons;\nthis payload follows the same rule from day one.\n\nIf a future change ships a stable public attribution shape\n(display name string, or a resolvable public id via a new\n``/external/v1/users`` endpoint), add that field here and on the\nin-app payload together so the wire shapes stay aligned across\nsurfaces."},"MeetingAttendeePublic":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"}},"type":"object","title":"MeetingAttendeePublic","description":"One meeting attendee.\n\nIncludes external guests who are not Sentra users. For those rows\n``email`` is the invited address and ``first_name`` / ``last_name`` are\n``null`` because no Sentra user record backs them."},"MeetingDetail":{"properties":{"meeting_id":{"type":"string","title":"Meeting Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"status":{"$ref":"#/components/schemas/PublicMeetingStatus","description":"Meeting processing status. `skipped` means a bot meeting where Sentra did not capture a usable recording/transcript (for example, the bot was not admitted or the captured audio had no meaningful content). This does not indicate whether the human user attended."},"privacy":{"type":"string","enum":["DEFAULT","PRIVATE"],"title":"Privacy","description":"Visibility scope for the meeting. ``DEFAULT`` means the meeting follows the org's default sharing (visible org-wide to admins; visible to attendees only otherwise). ``PRIVATE`` means the meeting is visible only to attendees, regardless of role."},"scheduled_start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scheduled Start Time","description":"RFC 3339 / ISO 8601 UTC timestamp with the literal `Z` suffix (e.g. `2026-05-22T12:34:56Z`)."},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"RFC 3339 / ISO 8601 UTC timestamp with the literal `Z` suffix (e.g. `2026-05-22T12:34:56Z`)."},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"RFC 3339 / ISO 8601 UTC timestamp with the literal `Z` suffix (e.g. `2026-05-22T12:34:56Z`)."},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"attendees":{"items":{"$ref":"#/components/schemas/MeetingAttendeePublic"},"type":"array","title":"Attendees","description":"All attendees on the meeting, including external guests who are not Sentra users. `first_name` and `last_name` are populated only for Sentra users; external guests carry `email` only."},"summary":{"anyOf":[{},{"type":"null"}],"title":"Summary","description":"Summary JSON as produced by the meeting summary pipeline."},"transcript":{"anyOf":[{},{"type":"null"}],"title":"Transcript","description":"Transcript payload as stored in S3. Shape is provider-defined."}},"type":"object","required":["meeting_id","status","privacy"],"title":"MeetingDetail"},"MeetingListItem":{"properties":{"meeting_id":{"type":"string","title":"Meeting Id","description":"Recall bot id; stable identifier."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"status":{"$ref":"#/components/schemas/PublicMeetingStatus","description":"Meeting processing status. `skipped` means a bot meeting where Sentra did not capture a usable recording/transcript (for example, the bot was not admitted or the captured audio had no meaningful content). This does not indicate whether the human user attended."},"privacy":{"type":"string","enum":["DEFAULT","PRIVATE"],"title":"Privacy","description":"Visibility scope for the meeting. ``DEFAULT`` means the meeting follows the org's default sharing (visible org-wide to admins; visible to attendees only otherwise). ``PRIVATE`` means the meeting is visible only to attendees, regardless of role."},"scheduled_start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scheduled Start Time","description":"RFC 3339 / ISO 8601 UTC timestamp with the literal `Z` suffix (e.g. `2026-05-22T12:34:56Z`)."},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"RFC 3339 / ISO 8601 UTC timestamp with the literal `Z` suffix (e.g. `2026-05-22T12:34:56Z`)."},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"RFC 3339 / ISO 8601 UTC timestamp with the literal `Z` suffix (e.g. `2026-05-22T12:34:56Z`)."},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"attendees_emails":{"items":{"type":"string"},"type":"array","title":"Attendees Emails","description":"Deduplicated emails of the meeting's attendees, including external guests who are not Sentra users. Case-insensitive dedup, first-seen order preserved. This is an email-only projection and does NOT map one-to-one to the meeting-detail `attendees` list: attendees with no resolvable email (e.g. a name-only Sentra user) are omitted, and a distinct address appears once here even though detail emits one entry per attendee row (with names). Use the meeting-detail endpoint for the full per-attendee list."},"user_was_attendee":{"type":"boolean","title":"User Was Attendee"}},"type":"object","required":["meeting_id","status","privacy","user_was_attendee"],"title":"MeetingListItem","description":"One row in the list-meetings response. Lean by design."},"MeetingListResponse":{"properties":{"meetings":{"items":{"$ref":"#/components/schemas/MeetingListItem"},"type":"array","title":"Meetings"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"limit":{"type":"integer","title":"Limit"},"has_more":{"type":"boolean","title":"Has More"}},"type":"object","required":["meetings","total","page","limit","has_more"],"title":"MeetingListResponse"},"MeetingProcessingResponse":{"properties":{"status":{"type":"string","const":"processing","title":"Status","default":"processing"},"detail":{"type":"string","title":"Detail"},"meeting_status":{"$ref":"#/components/schemas/PublicMeetingStatus"}},"type":"object","required":["detail","meeting_status"],"title":"MeetingProcessingResponse","description":"Body of the 409 returned while a meeting is still being processed."},"MeetingSourcePayload":{"properties":{"kind":{"type":"string","const":"meeting","title":"Kind","default":"meeting"},"org_meeting_id":{"type":"string","title":"Org Meeting Id"},"meeting_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meeting Name"},"meeting_privacy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meeting Privacy"},"meeting_bot_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meeting Bot Id"},"meeting_platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meeting Platform"}},"type":"object","required":["org_meeting_id"],"title":"MeetingSourcePayload"},"MeetingStatus":{"type":"string","enum":["joining","processing_raw","processing_async","in_progress","pending_speaker_mapping","completed","failed","skipped","duplicate"],"title":"MeetingStatus"},"MeetingUpstreamErrorResponse":{"properties":{"status":{"type":"string","const":"upstream_error","title":"Status","default":"upstream_error"},"detail":{"type":"string","title":"Detail"},"meeting_status":{"$ref":"#/components/schemas/PublicMeetingStatus"}},"type":"object","required":["detail","meeting_status"],"title":"MeetingUpstreamErrorResponse","description":"Body of the 502 returned when transcript storage is unreachable."},"MemoryAgentCitationResponse":{"properties":{"source_type":{"type":"string","title":"Source Type"},"title":{"type":"string","title":"Title"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timestamp"}},"type":"object","required":["source_type","title"],"title":"MemoryAgentCitationResponse","description":"A source citation backing the agent's answer."},"MemoryAgentRunAcceptedResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"$ref":"#/components/schemas/PublicMemoryAgentJobStatus"}},"type":"object","required":["job_id","status"],"title":"MemoryAgentRunAcceptedResponse","description":"Response for an accepted memory-agent run."},"MemoryAgentRunRequest":{"properties":{"prompt":{"type":"string","maxLength":4000,"title":"Prompt","description":"The natural-language task for the memory agent. Phrase it as a focused research question over your Sentra memory."}},"type":"object","required":["prompt"],"title":"MemoryAgentRunRequest","description":"Body for ``POST /external/v1/memory-agent/runs``."},"MemoryAgentRunStatusResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"$ref":"#/components/schemas/PublicMemoryAgentJobStatus"},"started_at":{"type":"string","title":"Started At"},"last_updated_at":{"type":"string","title":"Last Updated At"},"elapsed_seconds":{"type":"integer","title":"Elapsed Seconds"},"final_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Final Response"},"citations":{"anyOf":[{"items":{"$ref":"#/components/schemas/MemoryAgentCitationResponse"},"type":"array"},{"type":"null"}],"title":"Citations"},"finish_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish Reason"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["job_id","status","started_at","last_updated_at","elapsed_seconds"],"title":"MemoryAgentRunStatusResponse","description":"Polling response for a memory-agent run.\n\n``status`` plus the timing fields are present on every response so the body\nchanges each poll while in flight and a caller can tell a live run from a\nstuck one. ``final_response`` / ``citations`` populate on success, ``error``\non failure."},"OrgMember":{"properties":{"id":{"type":"integer","title":"Id","description":"Sentra user id — pass this as owner_user_id / a stakeholder_user_ids entry."},"display_name":{"type":"string","title":"Display Name","description":"Full name; a neutral placeholder when the name is blank (never the work email unless include_email=true)."},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Work email; populated only when the caller passes include_email=true."}},"type":"object","required":["id","display_name"],"title":"OrgMember","description":"One org member, projected for owner/stakeholder id resolution."},"OrgMemberDirectory":{"properties":{"members":{"items":{"$ref":"#/components/schemas/OrgMember"},"type":"array","title":"Members"}},"type":"object","required":["members"],"title":"OrgMemberDirectory"},"PersonalEmailSourcePayload":{"properties":{"kind":{"type":"string","const":"personal_email","title":"Kind","default":"personal_email"},"thread_id":{"type":"string","title":"Thread Id"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"sender_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Address"}},"type":"object","required":["thread_id"],"title":"PersonalEmailSourcePayload","description":"Source metadata for a chunk derived from a user's personal email thread.\n\nDistinct ``kind`` from :class:`EmailSourcePayload` so the two email\npipelines never share a wire shape.  Surfaces only display-safe fields —\n``thread_id`` (the Nylas thread id) plus the latest message's ``subject``\nand normalized ``sender_address``.  No recipient set is exposed: this\nsource is owner-only, so a chunk is only ever returned to the mailbox\nowner, but we still keep the public payload free of membership metadata\nto match :class:`EmailSourcePayload`'s contract."},"Provenance":{"type":"string","enum":["internal","external","unknown"],"title":"Provenance","description":"Where a Drive document came from, relative to the org.\n\nValues are the wire strings persisted on ``EntityMemoryFile.provenance`` and\nsurfaced to the agent, so the enum is a drop-in for the bare string."},"PublicMeetingStatus":{"type":"string","enum":["joining","processing_raw","processing_async","in_progress","pending_speaker_mapping","completed","failed","skipped","duplicate","unknown"],"title":"PublicMeetingStatus","description":"Closed set of meeting statuses exposed on the public API.\n\nMirrors the in-app :class:`backend.models.models.MeetingStatus`\nvalues 1:1, plus ``UNKNOWN`` for the defensive fallback when the\nunderlying row carries ``NULL`` or an unmapped value (legacy data,\nbackfill in flight, etc.).  Kept as its own enum rather than\nre-exporting ``MeetingStatus`` so an internal status added for\npipeline purposes (e.g. a new transient state) doesn't auto-leak\nonto the public contract — adding a new public value requires an\nexplicit edit here."},"PublicMemoryAgentJobStatus":{"type":"string","enum":["queued","running","completed","failed"],"title":"PublicMemoryAgentJobStatus"},"RawEmailMessage":{"properties":{"rfc822_message_id":{"type":"string","title":"Rfc822 Message Id","description":"RFC822 ``Message-ID`` header value. Stable across mailboxes and the canonical identifier for an individual email."},"internal_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Internal Date","description":"Gmail ``internalDate`` for the message — the time Gmail received it. Messages in the response are sorted ascending by this field."},"from_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Address","description":"Raw ``From:`` header value, including any display name."},"sender_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Address","description":"Normalized sender address (lowercased, display name stripped). May be ``null`` on rows that pre-date the normalization column; the raw ``from_address`` is always present when this is null."},"to":{"items":{"type":"string"},"type":"array","title":"To","description":"``To:`` recipients parsed from the original RFC822 headers. Order preserved as it appeared on the wire."},"cc":{"items":{"type":"string"},"type":"array","title":"Cc","description":"``Cc:`` recipients parsed from the original RFC822 headers."},"bcc":{"items":{"type":"string"},"type":"array","title":"Bcc","description":"``Bcc:`` recipients parsed from the original RFC822 headers when present. Gmail strips ``Bcc`` on inbound delivery so this list is best-effort: it is populated for messages stored from the sender's mailbox copy and empty for messages stored from any recipient mailbox."},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"body_text":{"type":"string","title":"Body Text","description":"Plain-text body. Prefers ``text/plain`` MIME parts; falls back to ``text/html`` with tags stripped when no plain part exists. Quoted reply chains are retained.","default":""},"body_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Html","description":"Original ``text/html`` body when one was present. Not sanitized — render in a sandbox if surfacing in a UI."},"headers":{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array","title":"Headers","description":"Full original RFC822 headers as a list of ``{name, value}`` pairs, preserving order and duplicates. Pulled directly from the stored Gmail payload."},"has_attachments":{"type":"boolean","title":"Has Attachments","default":false}},"type":"object","required":["rfc822_message_id"],"title":"RawEmailMessage","description":"One message inside a thread, projected for the public API."},"RawEmailThreadResponse":{"properties":{"thread_key":{"type":"string","title":"Thread Key","description":"Echo of the request path parameter. The same value is returned by ``/sources``-style citations referencing email facts."},"message_count":{"type":"integer","title":"Message Count","description":"Number of messages in this response page. Already filtered by per-message visibility — messages the caller lacks per-message access to are not included and not counted. May be zero on a non-final page when the page consists entirely of inaccessible messages that the visibility filter elides; ``next_cursor`` is what tells the caller more pages are available."},"messages":{"items":{"$ref":"#/components/schemas/RawEmailMessage"},"type":"array","title":"Messages"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor to fetch the next page; ``null`` when the response includes the final message in the thread the caller can see. Pass back unmodified as the ``cursor`` query parameter on the next request. The cursor advances over the underlying SQL result, not just visible rows, so a hostile caller cannot infer thread length or visibility from the page size."}},"type":"object","required":["thread_key","message_count","messages"],"title":"RawEmailThreadResponse","description":"Wire shape for ``GET /external/v1/emails/threads/raw``."},"SlackChannelDaySourcePayload":{"properties":{"kind":{"type":"string","const":"slack_channel_day","title":"Kind","default":"slack_channel_day"},"channel_id":{"type":"string","title":"Channel Id"},"channel_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Name"},"day":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Day"},"team_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Team Id"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["channel_id"],"title":"SlackChannelDaySourcePayload","description":"Source metadata for a chunk derived from a Slack (channel, day) bucket."},"SourceListItem":{"properties":{"source_type":{"type":"string","title":"Source Type","description":"Public alias usable as a `source_types` filter value (`meeting`, `file`, `manual`, `slack`, `github`, `linear`, `clickup`, `jira`, `erpnext`, `email`, `personal_email`, and the HubSpot object types: `hubspot_contact`, `hubspot_company`, `hubspot_deal`, `hubspot_ticket`, `hubspot_note`, `hubspot_task`, `hubspot_call`, `hubspot_meeting`, `hubspot_lead`, `hubspot_line_item`, `hubspot_quote`, `hubspot_custom_object`). Keep in lockstep with PUBLIC_SOURCE_TYPE_ALIASES."},"display_name":{"type":"string","title":"Display Name","description":"Human-readable label for the source."},"connected":{"type":"boolean","title":"Connected","description":"True iff data from this source is reachable for the org. `meeting` and `file` are always `true` (no integration to connect — see the route docstring); `slack`, `github`, `linear`, `clickup`, `jira`, `erpnext`, and the HubSpot object-type sources (`hubspot_contact` / `hubspot_company` / `hubspot_deal` / `hubspot_ticket` / `hubspot_note` / `hubspot_task` / `hubspot_call` / `hubspot_meeting` / `hubspot_lead` / `hubspot_line_item` / `hubspot_quote` / `hubspot_custom_object`) are `true` iff the org has connected that integration AND has at least one synced record of that object type (`erpnext` requires only that the integration is connected); `email` is `true` iff the org has email search enabled (org-wide Gmail DWD, not per-user OAuth). ORG-LEVEL signal — a given API key's user may still see zero rows if they are not a member of the underlying external workspace (Slack channel, Linear team, GitHub repo, ClickUp workspace), lack `BROWSE_PROJECTS` on the Jira issue's project, hold no ERPNext permission on the document, or are not a participant on any email thread."}},"type":"object","required":["source_type","display_name","connected"],"title":"SourceListItem","description":"One row in ``GET /sources``: a source the API knows about.\n\n``source_type`` is the public alias usable verbatim as a value for\nthe ``source_types`` filter on ``/entities/{id}/chunks`` and\n``/entities/{id}/relationships`` — no translation needed."},"SourceListResponse":{"properties":{"sources":{"items":{"$ref":"#/components/schemas/SourceListItem"},"type":"array","title":"Sources"}},"type":"object","required":["sources"],"title":"SourceListResponse"},"SwimlaneEventConfidence":{"type":"string","enum":["hard","soft"],"title":"SwimlaneEventConfidence","description":"Whether an event is an observed fact (``hard``) or an inferred judgment (``soft``).\n\nFalls out of the two-layer generation: an OBSERVED event stated in the data\nis ``hard``; an INFERRED judgment against expectation (a slip, a stall) is\n``soft`` and is badged, never presented as fact.\n\n@xref backend/services/swimlanes/schemas.py (SwimlaneActivityEvent.confidence) — the wire field this enum types.\n@xref backend/services/swimlanes/events.py (_VALID_EVENT_CONFIDENCE) — same literal set; the finder drops events outside it.\n@xref backend/models/swimlanes.py (SwimlaneEvent.confidence) — the column whose value is this enum."},"SwimlaneEventStatus":{"type":"string","enum":["active","superseded"],"title":"SwimlaneEventStatus","description":"Per-event content lifecycle for an Event Feed event.\n\nThe feed is read-only (no dismiss), so the lifecycle is only ``ACTIVE``\n(currently surfaced) and ``SUPERSEDED`` (a newer regen replaced it) — no\n``DISMISSED``. Nothing is hard-deleted; superseded rows are retained as\nhistory, exactly as :class:`SwimlaneWhatsNextItemStatus`.\n\n@xref backend/models/swimlanes.py (SwimlaneEvent.status) — the column this constrains (DB CHECK ck_swimlane_events_status)."},"SwimlaneEventType":{"type":"string","enum":["deploy_release","deadline_change","milestone","decision","scope_change","blocker","pilot_test","staffing_change","stall"],"title":"SwimlaneEventType","description":"The generic PM event taxonomy (EF-TAXONOMY), frozen for V1.\n\nThe value is the event ``type`` the finder emits and the card renders. One\nshared source of truth so the wire ``Literal``, the finder gate, and the\nOpenAI response schema cannot drift — a value one layer allows but another\nrejects would silently drop the event. The closed set is enforced in code\n(the finder gate + wire Literal + schema enum) AND by a DB CHECK\n(``ck_swimlane_events_type``).\n\n@xref backend/services/swimlanes/schemas.py (SwimlaneActivityEvent.type) — the wire field this enum types.\n@xref backend/services/swimlanes/events.py (_VALID_EVENT_TYPES) — same literal set; the finder drops events outside it.\n@xref backend/services/swimlanes/events_prompt.py (_DETECTION_PROMPT \"EVENT TYPES\") — hand-maintained copy of these values in the validated prompt text; update it when this taxonomy changes or the model never emits the new type.\n@xref backend/models/swimlanes.py (SwimlaneEvent.type) — the column whose value is this enum."},"TeamsChannelDaySourcePayload":{"properties":{"kind":{"type":"string","const":"teams_channel_day","title":"Kind","default":"teams_channel_day"},"channel_id":{"type":"string","title":"Channel Id"},"channel_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel Name"},"day":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Day"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["channel_id"],"title":"TeamsChannelDaySourcePayload","description":"Source metadata for a chunk derived from a Teams (channel, day) bucket.\n\n``channel_id`` is the Graph channelId; ``day`` is the 07:00-UTC-rolled\nbucket label. ``url`` is the opaque Teams ``webUrl`` taken from a message in\nthe bucket (``allChannels`` exposes no channel-level one). No member list is\nexposed — channel membership is access-control metadata, not a public field."},"TeamsChatDaySourcePayload":{"properties":{"kind":{"type":"string","const":"teams_chat_day","title":"Kind","default":"teams_chat_day"},"chat_id":{"type":"string","title":"Chat Id"},"chat_topic":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chat Topic"},"day":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Day"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["chat_id"],"title":"TeamsChatDaySourcePayload","description":"Source metadata for a chunk derived from a Teams (chat, day) bucket.\n\n``chat_id`` is the Graph chatId; ``day`` is the 07:00-UTC-rolled bucket\nlabel. ``url`` is the opaque Teams ``webUrl`` when the export carried one\n(may be null on some tenants). No participant/member list is exposed —\nchat membership is access-control metadata, not a public field."},"ThreadStallItem":{"properties":{"thread_key":{"type":"string","title":"Thread Key","description":"Pass to GET /external/v1/emails/threads/raw?thread_key=... to fetch the thread content (that endpoint enforces per-message email visibility)."},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"topic":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Topic","description":"<=6-word topic label from the scorer."},"computed_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Computed Date","description":"ISO date of the snapshot this row reflects."},"status":{"type":"string","title":"Status","description":"``OPEN``, ``CLOSED`` (RESOLVED_SUBSTANTIVE), or ``UNKNOWN`` (scoring failed for this thread — impact/urgency/scope are null)."},"resolution":{"type":"string","title":"Resolution","description":"Thread-verifier resolution state: ``NO_RESPONSE`` / ``HOLDING_RESPONSE`` / ``RESOLVED_SUBSTANTIVE``, or ``UNSCORED`` when scoring failed for this thread (surfaced as ``status`` ``UNKNOWN``)."},"message_count":{"type":"integer","title":"Message Count"},"distinct_senders":{"type":"integer","title":"Distinct Senders"},"days_quiet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Days Quiet","description":"Days since the thread's last message (staleness)."},"span_days":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Span Days","description":"Days from first to last message in the window."},"impact":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impact","description":"LLM impact 1-10, or null."},"urgency":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Urgency","description":"LLM urgency 1-10, or null."},"scope":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Scope","description":"LLM scope 1-10, or null."},"crit_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Crit Score","description":"Criticality score from the LLM impact/urgency/scope judgments with aging. Higher is more critical. Null when scoring failed."},"stall_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Stall Score","description":"Deterministic stall severity — how long a big, multi-party thread has gone quiet. Higher is more stalled."},"combined_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Combined Score","description":"Blended rank key combining criticality and stall severity (normalized 0-1). Items are ordered by this within open-vs-closed."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason","description":"One-sentence criticality rationale from the scorer."},"resolution_evidence":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Evidence","description":"One-sentence note on the thread's latest state."}},"type":"object","required":["thread_key","status","resolution","message_count","distinct_senders"],"title":"ThreadStallItem","description":"One ranked thread in the latest snapshot."},"ThreadStallListResponse":{"properties":{"org_id":{"type":"integer","title":"Org Id"},"count":{"type":"integer","title":"Count","description":"Rows visible to the caller."},"as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of","description":"ISO date requested via ``as_of``; null for today's latest. Rows are the newest snapshot on or before this date."},"items":{"items":{"$ref":"#/components/schemas/ThreadStallItem"},"type":"array","title":"Items"}},"type":"object","required":["org_id","count","items"],"title":"ThreadStallListResponse","description":"Wire shape for ``GET /external/v1/thread-stalls``."},"UpdateSwimlaneBody":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"goal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal"},"owner_user_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Owner User Id"},"stakeholder_user_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Stakeholder User Ids"}},"type":"object","title":"UpdateSwimlaneBody","description":"Partial update — every field optional, but at least one must be sent.\n\n``model_fields_set`` distinguishes an omitted field from an explicit null so\n``owner_user_id: null`` clears the owner while an absent key leaves it."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"ZendeskTicketSourcePayload":{"properties":{"kind":{"type":"string","const":"zendesk_ticket","title":"Kind","default":"zendesk_ticket"},"ticket_id":{"type":"string","title":"Ticket Id"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"priority":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Priority"},"group_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group Name"},"organization_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization Name"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["ticket_id"],"title":"ZendeskTicketSourcePayload","description":"Source metadata for a chunk derived from a Zendesk ticket.\n\n``ticket_id`` is the Zendesk ticket's own id, which is unique within an\naccount — not a composite ingest key, so it is safe to render.\n\nNo visibility key is exposed. Unlike the Basecamp payload, which renders its\n``project_id`` because clients group by project, a Zendesk ticket's group and\nassignee move constantly and both visibility passes read them live from the\nmirror rather than from a chunk. Rendering a stale copy would invite a client\nto group or filter on something the ACL no longer agrees with.\n\nThis payload carries no satisfaction rating and no SLA or reply-time figures;\nthose are mirrored but never surfaced.\n\n@xref backend/api/entity_memory.py (ZendeskTicketSourcePayload) — this external payload is a deliberate subset of the internal API shape; changes to the internal payload must keep this in sync\n@xref backend/services/zendesk_ticket_render.py (render_ticket_text) — the field rule this describes; update both together."}},"securitySchemes":{"BearerAuth":{"type":"http","description":"Personal Sentra API key, passed as a standard ``Authorization: Bearer sk_sentra_…`` value. Generate a key in Settings → API Access; the raw value is shown only once at creation. Keys inherit the owning user's meeting access and organization scope.","scheme":"bearer","bearerFormat":"sk_sentra_*"}}}}