# Inbox

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 25-28 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 29-35 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 36-42 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 43-50 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 51-57 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 58-63 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 64-102 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 103-119 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 120-129 |
| [Verification](#verification) | Evidence required before reporting success. | 130-135 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 136-142 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 143-149 |
<!-- END GENERATED: guide-map -->

## Purpose

Use this reference for the sequence reply inbox: unread reply digests, group triage, message/thread inspection, read-state changes, and explicit reply sends through `vf.inbox.*`.

## Use This When

- You need to list sequence conversation groups that are unread, need reply, replied, or all.
- You need to inspect inbound messages before deciding whether a sequence prospect needs a response.
- You need to mark a conversation group read or unread as an operator-facing state change.
- You need to send a reply on an existing reply-capable conversation thread after explicit user confirmation.

## Do Not Use This For

- Sequence lifecycle, audience staging, publishing, activation, analytics, or destination sync. Use `sequences.md`.
- LinkedIn browser inbox scraping or rendered chat history from a logged-in browser. Use `linkedin.md` or `host_browser.md`.
- Copy quality, reply drafting, human voice, or personalization judgment. Use `copy.md`.
- Internal AI state on inbox groups. The SDK intentionally does not expose the `/ai-state` route in v1.

## Mental Model

Groups are the triage unit. A group contains one or more threads, and threads contain messages. Use `vf.inbox.groups(...)` to find groups, then `vf.inbox.group_messages(...)` and `vf.inbox.group_threads(...)` for evidence and reply routing.

Unread state derives from inbound counts: `unread_count = inbound_message_count - last_read_inbound_count`. `needs_reply` is the product triage state for groups where the latest conversation context still needs an operator or agent response.

Messages are ordered newest-first by `occurred_at`. Pending outbound replies can appear with message reads, but only on the first page.

## Preflight

- Resolve the workspace first when there is any ambiguity. Pass `org_id=` only when intentionally overriding the focused workspace.
- For digests, use `status="unread"` and `order="newest"` with a persisted high-water mark. Do not use operator read state as digest memory.
- Before sending, read the group and threads, confirm the target thread is reply-capable, and get explicit user confirmation immediately before `vf.inbox.send_reply(...)`.
- Use `client_request_id` for reply sends so retries are idempotent. A duplicate key returns 409.

## Risk And Side Effects

- `vf.inbox.groups`, `group`, `group_messages`, `group_threads`, and `reply_status` are platform reads.
- `vf.inbox.mark_group_read(...)` and `vf.inbox.mark_group_unread(...)` change operator-visible unread state.
- `vf.inbox.send_reply(...)` is a customer-visible send. It requires explicit user confirmation immediately before the call. Preserve the returned `run_id` and poll with `vf.inbox.reply_status(...)`.

## Canonical Operating Pattern

Unread digest core (where the enclosing Play loads `window_start_iso` and the
per-group `high_water` mapping from durable checkpoint state):

```python
groups = vf.inbox.groups(
    status="unread",
    order="newest",
    from_time=window_start_iso,
    limit=200,
)

for group in groups["groups"]:
    messages = vf.inbox.group_messages(group["conversation_group_id"], limit=200)
    inbound = [
        message
        for message in messages["messages"]
        if message.get("direction") == "inbound"
        and (message.get("occurred_at"), message.get("conversation_message_id")) > high_water[group["conversation_group_id"]]
    ]
```

For a recurring digest, make the enclosing Play own the checkpoint. Initialize
an explicit first-run window, load a high-water mark per group as
`(occurred_at, conversation_message_id)`, and advance those marks plus global
`last_check_at` only after the digest output is durably published. Route the
checkpoint and restart behavior to `workflow_sources.md`; do not use Slack or
operator read state as workflow memory, and do not call `mark_group_read` from
a digest that only reports new replies.

Reply send:

1. Read `vf.inbox.group(group_id)` and `vf.inbox.group_threads(group_id, reply_capable_only=True)`.
2. Draft and review copy through `copy.md` if the reply text is not already user-approved.
3. Ask for explicit confirmation immediately before the send.
4. Call `vf.inbox.send_reply(thread_id, body_text=..., client_request_id=...)`.
5. Preserve `run_id` and poll `vf.inbox.reply_status(run_id)`.

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.inbox.group` | Read one conversation group with its current detail payload. | <strong>Required</strong>: `conversation_group_id` (int; required): Conversation group id to read.<br><strong>Optional</strong>: `org_id` (str \| None; optional; default `None`): Optional workspace override for the inbox read. | `inbox_group_detail` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.group_messages`, `vf.inbox.group_threads` | read | installed SDK: `verbiflow/inbox.py:214-249` |
| `vf.inbox.group_messages` | Page group messages newest-first by occurred_at. Pending outbound replies are included only on the first page. | <strong>Required</strong>: `conversation_group_id` (int; required): Conversation group id whose messages should be paged.<br><strong>Optional</strong>: `cursor` (str \| None; optional; default `None`): Opaque message cursor from the prior page.; `limit` (int; optional; default `100`): Page size, validated client-side to 1..200.; `org_id` (str \| None; optional; default `None`): Optional workspace override for the inbox read. | `inbox_messages` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.group_messages` | read | installed SDK: `verbiflow/inbox.py:251-301` |
| `vf.inbox.group_threads` | List conversation threads for a group, optionally reply-capable only. | <strong>Required</strong>: `conversation_group_id` (int; required): Conversation group id whose threads should be listed.<br><strong>Optional</strong>: `org_id` (str \| None; optional; default `None`): Optional workspace override for the inbox read.; `reply_capable_only` (bool; optional; default `False`): When true, return only threads that can accept replies. | `inbox_threads` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.send_reply` | read | installed SDK: `verbiflow/inbox.py:366-407` |
| `vf.inbox.groups` | List sequence reply conversation groups for triage and reply digests. | <strong>Required</strong>: none<br><strong>Optional</strong>: `channels` (list[str] \| None; optional; default `None`): Optional channel filters sent as repeated query params.; `cursor` (str \| None; optional; default `None`): Opaque cursor from a prior page using the same order.; `from_time` (str \| None; optional; default `None`): Optional ISO 8601 lower bound for group last_message_at, sent as `from`.; `limit` (int; optional; default `50`): Page size, validated client-side to 1..200.; `order` (priority \| newest; optional; default `'priority'`): Group ordering. Cursors are bound to this value.; `org_id` (str \| None; optional; default `None`): Optional workspace override for the inbox read.; `search` (str \| None; optional; default `None`): Optional group/person search text.; `sender_account_ids` (list[str] \| None; optional; default `None`): Optional sender account filters sent as repeated query params.; `sequence_ids` (list[str] \| None; optional; default `None`): Optional sequence filters sent as repeated query params.; `status` (all \| unread \| needs_reply \| replied; optional; default `'needs_reply'`): Conversation group status filter.; `to_time` (str \| None; optional; default `None`): Optional ISO 8601 upper bound for group last_message_at, sent as `to`. | `inbox_groups` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.group_messages`, `vf.inbox.group`, `vf.inbox.group_threads` | read | installed SDK: `verbiflow/inbox.py:40-142` |
| `vf.inbox.iter_group_messages` | Iterate a conversation group's messages across opaque cursor pages. | <strong>Required</strong>: `conversation_group_id` (int; required): Conversation group whose messages should be iterated.<br><strong>Optional</strong>: `max_messages` (int \| None; optional; default `None`): Optional maximum total number of messages to yield.; `org_id` (str \| None; optional; default `None`): Verbiflow organization/workspace UUID.; `page_size` (int; optional; default `200`): Number of messages requested per page, from 1 through 200. | `inbox_message_stream` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.send_reply` | read | installed SDK: `verbiflow/inbox.py:303-364` |
| `vf.inbox.iter_groups` | Iterate conversation groups across opaque cursor pages. | <strong>Required</strong>: none<br><strong>Optional</strong>: `channels` (list[str] \| None; optional; default `None`): Channels parameter for `inbox.iter_groups`.; `from_time` (str \| None; optional; default `None`): From time parameter for `inbox.iter_groups`.; `max_groups` (int \| None; optional; default `None`): Optional maximum total number of groups to yield.; `order` (InboxOrder; optional; default `'priority'`): Conversation group order.; `org_id` (str \| None; optional; default `None`): Verbiflow organization/workspace UUID.; `page_size` (int; optional; default `200`): Number of groups requested per page, from 1 through 200.; `search` (str \| None; optional; default `None`): Optional text filter for enum/list APIs.; `sender_account_ids` (list[str] \| None; optional; default `None`): Sender account ids parameter for `inbox.iter_groups`.; `sequence_ids` (list[str] \| None; optional; default `None`): Sequence ids parameter for `inbox.iter_groups`.; `status` (InboxStatus; optional; default `'needs_reply'`): Conversation status filter.; `to_time` (str \| None; optional; default `None`): To time parameter for `inbox.iter_groups`. | `inbox_group_stream` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.group_messages`, `vf.inbox.group_threads` | read | installed SDK: `verbiflow/inbox.py:144-212` |
| `vf.inbox.mark_group_read` | Change the user-visible unread state by marking a group read. A digest that only reports should not call this. | <strong>Required</strong>: `conversation_group_id` (int; required): Conversation group id whose unread state should be updated.<br><strong>Optional</strong>: `last_read_message_id` (int \| None; optional; default `None`): Optional message id to mark through; null marks through latest.; `org_id` (str \| None; optional; default `None`): Optional workspace override for the read-state write. | `inbox_read_state` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.groups` | platform_write | installed SDK: `verbiflow/inbox.py:409-455` |
| `vf.inbox.mark_group_unread` | Change the user-visible unread state by marking a group unread. | <strong>Required</strong>: `conversation_group_id` (int; required): Conversation group id whose unread state should be updated.<br><strong>Optional</strong>: `org_id` (str \| None; optional; default `None`): Optional workspace override for the read-state write. | `inbox_read_state` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.groups` | platform_write | installed SDK: `verbiflow/inbox.py:457-493` |
| `vf.inbox.reply_status` | Read the status of a queued inbox reply run. | <strong>Required</strong>: `run_id` (str; required): Reply run id returned by inbox.send_reply.<br><strong>Optional</strong>: `org_id` (str \| None; optional; default `None`): Optional workspace override for the reply-status read. | `inbox_reply_status` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.reply_status` | read | installed SDK: `verbiflow/inbox.py:577-612` |
| `vf.inbox.send_reply` | Send a real customer-visible reply and preserve the returned run_id. Requires explicit user confirmation immediately before the call. client_request_id makes retries idempotent; duplicates return 409. | <strong>Required</strong>: `body_text` (str; required): Plain-text reply body. Must be non-empty.; `conversation_thread_id` (int; required): Conversation thread id that will receive the reply.<br><strong>Optional</strong>: `bcc` (list[str] \| None; optional; default `None`): Optional non-empty BCC recipient strings.; `body_html` (str \| None; optional; default `None`): Optional HTML reply body for email-capable threads.; `cc` (list[str] \| None; optional; default `None`): Optional non-empty CC recipient strings.; `client_request_id` (str \| None; optional; default `None`): Optional idempotency key. Duplicate keys return 409.; `org_id` (str \| None; optional; default `None`): Optional workspace override for the reply send.; `subject` (str \| None; optional; default `None`): Optional email subject override. | `inbox_reply_accepted` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.inbox.reply_status` | platform_write | installed SDK: `verbiflow/inbox.py:495-575` |
<!-- END GENERATED: core-sdk-surfaces -->

## Data Contracts

- Group keys include `conversation_group_id`, `person_id`, identity fields, `last_message_at`, `last_inbound_at`, `last_outbound_at`, `preview`, `last_channel`, `latest_thread_id`, `needs_reply`, counts, channel/sender/sequence arrays, `updated_at`, and `version`.
- Thread keys include `conversation_thread_id`, `conversation_group_id`, `channel`, sender/account/provider fields, `subject`, `reply_capable`, `can_reply`, `history_only_reason`, message counts/timestamps, `updated_at`, and `version`.
- Message keys include `conversation_message_id`, group/thread/person ids, `channel`, `direction`, sender/from/to/cc/bcc fields, subject/body/preview, provider/source/sequence/runtime ids, `occurred_at`, `ingested_at`, and `created_at`.
- Message pages order newest-first by `occurred_at`, then message id.
- Group `from_time` and `to_time` map to query params `from` and `to`; they filter `last_message_at`.
- Cursors are opaque and order-bound. A cursor minted under `order="priority"` returns 400 if reused with `order="newest"`, and vice versa.
- The SDK intentionally omits advanced list filters (`reply_categories`, `buckets`, `priority_bands`, `urgencies`, `recommended_actions`) in v1.

## Verification

- For digest reads, print group count, next cursor, and the newest kept inbound message id per group.
- For read-state writes, re-read the group list or detail and verify unread state changed as intended.
- For sends, preserve `run_id`, call `vf.inbox.reply_status(run_id)`, and report status/phase/error fields.

## Troubleshooting

- 400 on paged groups often means cursor/order mismatch. Restart paging with the same `order` value used to mint the cursor.
- 409 on `send_reply` with duplicate `client_request_id` means the retry was deduplicated. Fetch the original run status if you preserved the prior `run_id`.
- 409 on `send_reply` for a thread can mean the thread is disconnected, not reply-capable, or lacks valid latest inbound/provider state. Re-read `group_threads(..., reply_capable_only=True)`.
- Empty digest results do not prove there were no replies; confirm the persisted high-water and `from_time` window before widening the query.

## Related References

- `sequences.md` for sequence lifecycle, audience, analytics, staging, publish, activation, and destination sync.
- `sequence_authoring.md` for draft sequence design.
- `copy.md` for reply text quality and approval-ready copy.
- `linkedin.md` and `host_browser.md` for LinkedIn/browser inbox work outside the sequence reply inbox.
- `safety.md` for customer-visible mutation confirmation gates.
