email-listener
Gmail webhook → CoS intent → fan-out. Email becomes a first-class trigger alongside Telegram.
Why this exists
Pi-CEO already creates sessions on Linear and GitHub webhook events (app/server/routes/webhooks.py). Email is conspicuously missing — it's the only inbox where "act on this for me" is the dominant interaction mode. Without email-listener, the user has to forward every email to Telegram or paste it manually. With email-listener, a labelled email becomes a session.
Trigger model
Push, not poll. Use Gmail's push-notifications via Pub/Sub — Gmail watches for label changes; Pub/Sub forwards to Pi-CEO's webhook endpoint within seconds.
Filter by label, not by every message. A new label pi-ceo/inbox is the trigger. User applies it (manually, or via a Gmail filter) when they want Pi-CEO to handle a thread. No global "act on every email" mode — too dangerous, too noisy.
Flow
1. User adds Gmail label pi-ceo/inbox to a thread (manual or filter)
2. Gmail Pub/Sub publishes a watch notification
3. Pub/Sub → POST /api/webhook/gmail (NEW route in app/server/routes/webhooks.py)
4. Handler: fetch full thread via Gmail API, extract:
- sender, recipients, subject, latest message body
- any attachments (download metadata only; don't fetch bodies in Wave 2)
- thread URL for the audit log
5. Compose a synthetic intent payload:
{
"intent": "email",
"sender": "...",
"subject": "...",
"body_summary": "<first 500 chars>",
"thread_url": "...",
"raw_thread_id": "..."
}
6. Route to intent-parser with hint=email — intent-parser maps to:
- "reply" if email shape is question/request to user
- "research" if email is a research dispatch
- "ticket" if email contains explicit ticket-creation language
- "unknown" otherwise → CoS asks user via Telegram review chat
7. Downstream: Scribe drafts reply (if reply) → telegram-draft-for-review →
user 👍 → Scribe sends via Gmail API
Contract
Inputs: Gmail Pub/Sub watch notification on label pi-ceo/inbox.
Outputs: synthetic intent payload routed to intent-parser; new session in Pi-CEO with triggered_by=email.
Safety bindings
- Auth scope: read-only Gmail access for fetching, send-as for replies. Never delete, never modify labels other than
pi-ceo/inbox/processed(added on completion). Never modify other labels. - Sender allowlist (recommended). A new env var
EMAIL_LISTENER_SENDER_ALLOWLIST(comma-sep) restricts which senders can trigger sessions. Default empty = trigger on any sender. Recommendation: populate before launch — without it, an attacker who knows your address can add the label via a shared inbox and trigger a session. - PII redactor (Wave 2 sibling skill) MUST run between step 4 and step 5. Email content is the highest-PII surface in the system.
- Reply-all guard. Scribe never auto-CCs the original recipient list. User must explicitly approve recipients on each draft.
- Out-of-band auth requests (any email asking Pi-CEO to "approve a pairing" or "add to allowlist") → automatically classified
unknownand surfaced to user. Same defense as Telegram MCP injection-defense rule.
Where the auth lives
- Gmail OAuth credentials → Composio substrate (
phill.mcgurk_workspace). Usecomposio-cloud-routineskill pattern. Tokens never written to disk on Pi-CEO server. - Pub/Sub topic + subscription → set up in GCP project. Topic name:
pi-ceo-gmail-watch. Subscription pushes tohttps://pi-dev-ops-production.up.railway.app/api/webhook/gmail. - Watch lifecycle: re-call
users.watchevery 7 days (Gmail expires watches after that). Cron job inapp/server/cron_scheduler.py— flag for setup.
Verification
- Apply
pi-ceo/inboxlabel to a test email manually. - Within 60s, expect: new session in Pi-CEO with
triggered_by=email,swarm.jsonlentry, intent classified. - If reply intent: Scribe drafts → telegram-draft-for-review fires → review chat receives draft.
- 👍 → Gmail API send → audit log.
- Test the sender allowlist guard: email from a non-allowlisted sender → no session created, log entry only.
- PII test: email containing a credit card number → redacted before reaching intent-parser; raw value never persisted.
When NOT to use
- Newsletters, marketing, automated mail — these should not have the
pi-ceo/inboxlabel. Filter at the Gmail-rule level, not at Pi-CEO. - Bulk operations ("reply to all 30 unread") — Wave 3 if needed; intentionally not in Wave 2 to avoid runaway-loop risk.
- Calendar invites — those route through
calendar-watcher, not email-listener.
Out of scope for Wave 2
- Outbound email initiation (Pi-CEO sending the first email of a thread) — Wave 3.
- Attachment processing (read PDFs, parse XLSX) — Wave 3. Wave 2 records attachment metadata only.
- Email read-receipts, scheduling sends, drafts in Gmail — Wave 3.
References
- Topology:
~/Pi-CEO/Second-Brain-Agent-Topology-2026-05-01.md - Existing webhook router:
Pi-Dev-Ops/app/server/routes/webhooks.py - Composio substrate memory:
~/.claude/projects/-Users-phill-mac-Pi-CEO/memory/project_composio_substrate.md - Gmail push notifications: https://developers.google.com/gmail/api/guides/push