Part 27 of 40 · 5 min read
Connect the mailbox and CRM without losing ownership of the facts
Experimental · Work in progress · Internal R&D
Incremental mailbox ingestion, contact identity, field ownership, draft review and uncertain sends.
Arunank Sharan · Kuzushi Labs ·
In this article
A solo business often starts in an inbox. An enquiry arrives, the founder recognises the company, checks the CRM, asks a question and eventually sends a proposal. Connecting an agent to that workflow should reduce the copying and searching without allowing an email thread to redefine commercial terms by accident.
The source prototype already has a supplied-payload Gmail ingestion path and simulated CRM behaviour. The next adapter needs real account authorisation, incremental synchronisation, provider receipts and a clear answer to which system owns each field. This chapter specifies that adapter rather than calling the simulation a live integration.
Ingest a notification as a hint
A mailbox notification is a reason to fetch changes, not the complete business event. Persist the account's sync cursor and advance it only after the fetched changes are durably ingested. Keep provider message identity separate from the business enquiry identity: several messages may describe one opportunity.
Gmail push notifications use history identifiers; a consumer follows the history feed to retrieve changes. Watches expire and need renewal, and a history cursor can become unusable, requiring a new synchronisation. Build both renewal and recovery into the connector. Gmail push guide.
Mailbox notification
→ verify the delivery channel
→ load account sync cursor
→ fetch provider changes
→ persist source messages and ingestion identities
→ advance cursor after durable ingestion
→ emit business candidates
If the process crashes before advancing the cursor, fetching the same changes again should be harmless. If the cursor advances first, the platform may permanently skip an enquiry. That ordering is more important than whether the classifier is a sophisticated agent.
Resolve identity without guessing too much
Normalise email addresses for matching while retaining the original address. Treat display names as weak evidence. A shared mailbox, forwarding address or consultant representing multiple companies can defeat a simplistic “email domain equals customer” rule.
Use provider message IDs for ingestion deduplication, thread IDs for navigation and a separate customer/contact mapping for business identity. When an inbound sender maps to several accounts, queue an identity review. Do not let the model choose a high-value customer merely because the names look similar.
Attachments become evidence with content hashes, size limits and safe parsing. Their text is untrusted input. An attached proposal can inform the opportunity, but instructions embedded in that proposal cannot authorise a tool call or overwrite the founder's pricing policy.
Give each CRM field an owner
The CRM may own contact details and sales stages while the operating ledger owns approval state and payment allocation. Document the ownership field by field. Bidirectional synchronisation without ownership rules creates loops in which each system repeatedly “corrects” the other.
The reference merger protects a set of locally owned fields and requires the expected remote revision:
from company_lab.connectors import merge_crm
local = {"status": "REVIEWED", "name": "Mira", "revision": 2}
remote = {"status": "NEW", "name": "Mira Tan", "revision": 3}
merged = merge_crm(local, remote,
owned_fields={"status"}, expected_revision=3)
assert merged["status"] == "REVIEWED"
assert merged["name"] == "Mira Tan"
This small function demonstrates an ownership rule. A real adapter also needs the provider's conditional-update mechanism, conflict responses and the mapping between provider revisions and local versions. If the provider offers no conditional write, the adapter must make that limitation explicit and reconcile competing changes.
Drafting and sending are different capabilities
A sales agent can read permitted messages, retrieve the current opportunity and prepare a draft. The send action separately checks the recipient, approved document revision, current suppression state and allowed communication purpose. The inbox sender is not automatically a permitted outbound recipient for every campaign.
Store the exact approved message or a digest of its immutable content and attachments. If the agent edits the price, recipient or attachment after review, invalidate the approval. A cosmetic preview should display the actual content to be sent, not a summary that hides changed terms.
The public connector simulator shows a send with a lost acknowledgement:
from company_lab.store import Store
from company_lab.connectors import FakeProvider, dispatch
store, provider = Store(), FakeProvider()
payload = {"to": "[email protected]", "quote_revision": "r3"}
first = dispatch(store, provider, "studio", "send-017", payload, lose_ack=True)
assert first["status"] == "UNKNOWN"
second = dispatch(store, provider, "studio", "send-017", payload)
assert second["status"] == "CONFIRMED"
assert len(provider.effects) == 1
The fake provider supports lookup by operation identity. Do not assume that arbitrary mailbox APIs provide the same guarantee. If reconciliation is inconclusive, preserve uncertainty and ask for a targeted check rather than blindly sending again.
Keep credentials behind the adapter
The agent selects a connection alias such as sales-mailbox; it never receives OAuth refresh tokens. Store credentials in a secret manager, bind the connection to the tenant and approved scopes, and record which connection executed the action. Revoking the connection must stop queued work at dispatch time.
Distinguish expired authorisation from a temporary network error. Repeatedly retrying a revoked mailbox wastes resources and hides the need for the founder to reconnect it. The cockpit should show the affected workflows and the last successful sync.
A useful first live pilot
Begin with inbound ingestion and draft creation. Compare the agent's identity match, qualification and proposed reply against the founder's actual decision. Keep sending manual until the retrieval and approval boundaries behave consistently.
The fixture suite should include a duplicate notification, a missing-history recovery, a forwarded enquiry, two contacts with the same name, a CRM edit during review and a send timeout. Track missing enquiries, duplicate records, stale-field overwrites and incorrect recipients separately. These are more useful launch criteria than the number of emails an agent can summarise per minute.