By use case
These are the workflows agents actually run against this twin, each naming the MCP tools and REST surfaces involved. Anything named here that is not fully modelled says so in place.Triage an unread inbox
List unread messages, read one, and move it out of the inbox. MCP:get_message, label_message, unlabel_message. REST: GET /gmail/v1/users/{userId}/messages, GET /gmail/v1/users/{userId}/messages/{id}, POST /gmail/v1/users/{userId}/messages/{id}/modify, POST /gmail/v1/users/{userId}/messages/batchModify.
Draft and send a reply
Prepare a reply without sending it, then send once it is approved. MCP:create_draft, list_drafts. REST: POST /gmail/v1/users/{userId}/drafts, GET /gmail/v1/users/{userId}/drafts, POST /gmail/v1/users/{userId}/drafts/send, POST /gmail/v1/users/{userId}/messages/send.
Organize threads and mailboxes with labels
Apply a label to a whole thread, including the sensitive labels a coach workflow gates on, and manage the label taxonomy itself. MCP:label_thread, unlabel_thread, apply_sensitive_thread_label, apply_sensitive_message_label, create_label, list_labels. REST: GET /gmail/v1/users/{userId}/labels, POST /gmail/v1/users/{userId}/labels, PATCH /gmail/v1/users/{userId}/labels/{id}, DELETE /gmail/v1/users/{userId}/labels/{id}, POST /gmail/v1/users/{userId}/threads/{id}/modify.
Search and read a thread’s history
Find a thread by query, read every message in it, and check what changed since a given history id. MCP:search_threads, get_thread. REST: GET /gmail/v1/users/{userId}/threads, GET /gmail/v1/users/{userId}/threads/{id}, GET /gmail/v1/users/{userId}/history.
Manage mailbox settings
Read filters, forwarding addresses, and send-as identities, and edit filters. Forwarding addresses and send-as identities are read-only — there is no create or update route for either. REST:GET /gmail/v1/users/{userId}/settings/filters, POST /gmail/v1/users/{userId}/settings/filters, DELETE /gmail/v1/users/{userId}/settings/filters/{id}, GET /gmail/v1/users/{userId}/settings/forwardingAddresses, GET /gmail/v1/users/{userId}/settings/sendAs.
What you can rely on
Each surface below carries two rulings that are deliberately kept apart: heat is how deep it should be, ruled per milestone; state is how deep it is today, measured by the twin’s own tests. Neither moves because the other did. A surface is in exactly one state, and the three below are different answers to “can I write a task against this”:@pome-sh/twin-gmail declares 13 MCP tools and 54 REST surfaces, read from the twin’s own fidelity.inventory.json (last updated 2026-08-10):
Out of scope
Real SMTP delivery, Pub/Sub push, Google OAuth and OIDC, and the Calendar, Drive, Contacts and admin surfaces are not modelled — send is a mailbox-state transition and no mail ever leaves. The bearer token is the Pome JWT, not a Google OAuth token, so there are no consent screens, refresh tokens, or scopes. These surfaces are named rather than left to the catch-all, so the 501 is documented and test-backed:Known divergence, ruled
8 divergences are on the ledger for Gmail: measured against the real API, found to differ, reviewed, and accepted. Each is registered and reverse-tested, so one that upstream heals becomes a signal rather than a surprise. The numbers are stable identifiers, not positions — a retired divergence leaves its number behind, so a gap in this list is a divergence that closed.
Where it runs. Both doors serve the same twin:
pome twin start gmail boots it as a local process, and pome sandbox create --twin gmail starts a hosted sandbox running the same image. The states above are properties of the twin, so they hold on either.
Everything above is what the twin declares. What it was measured to do is a different number: the comparison against real Gmail runs daily and publishes, surface by surface, how many matched, which drifted, and which are ruled exceptions with the reason written out in full — at status.pome.sh/twin/gmail, which also states when it last ran and how old the captured baseline it compares against is. No count is copied onto this page, because copying one is how it goes stale.
Read from FIDELITY.md, fidelity.inventory.json, LIMITS.md and REFERENCE-DIVERGENCES.md in pome-sh/digital-twins, at the commit .github/twins-ref pins (2e41939). LIMITS.md is the one to read before writing a task that pages, batches or attaches — every bound it names is enforced and fails loudly rather than truncating. REFERENCE-DIVERGENCES.md records which oracle each ruling was measured against.
Quickstart
Point your agent at it
For interactive development without a full task run, start the standalone twin:POME_GMAIL_REST_URL is the REST root. POME_GMAIL_MCP_URL is the MCP transport endpoint. POME_AUTH_TOKEN is a short-lived JWT that authenticates every request on the sandbox, and POME_GMAIL_TOKEN is an alias of it.
The default mailbox is pome-agent@pome-twin.test. Mailbox resolution accepts only me or the exact seeded gmail_email — anything else is rejected.
The session path /s/standalone/healthz requires the JWT and returns 401 without it.
To check whether the twin is alive, use the unauthenticated root health endpoint:
Task seed shape
Gmail task seeds are flat, with a requiredprimaryMailbox, an optional mailboxes array, a deliveryMode, a clock and named faults.
Generate it; do not copy it. The block below is what the twin starts with, printed by the CLI from the twin’s own declared state — so it parses against the twin you are about to seed, today and after the twin changes:
twins: ["gmail"] in the task’s ## Config block and name the file <task>.seed.json, beside the task’s .md. That ## Config is the only place the twin gets named: the file above is flat, so it carries no twin id of its own.
Seed file
A task is not the only way in.pome twin start and pome sandbox create take that very same file — no wrapper, no second shape, nothing to convert — and each takes the twin’s name beside it, since the file cannot supply one:
pome twin new-seed gmail slack writes it:
twin start and sandbox create want the name as well, and why a task’s sidecar does not — the ## Config beside it already named the twin. From two twins up the file is a per-twin envelope { <twin>: <seed> }, which does name its twins, though twin start still has to be told which one of them to boot. Replace, not merge, applies wherever the file lands.
Build your own world is that job end to end — generating the file, editing it down, both doors, and how to tell whether every field you wrote actually landed.
Example tasks
Ready-made examples you can run or copy to see the twin in action:Catalog
Run one
pome-suggest-tasks and pome-run-task skills, installed by the graded capstone.