Contacts
Channel-identity contacts distilled from conversation history: what a contact is, how they're created, and the contacts.* tool family.
Contacts
A contact is a channel identity: one row per workspace + channel + external identifier (an email address, a WhatsApp/SMS phone number, an Instagram handle, a Telegram id). The same person messaging on WhatsApp and emailing separately is two contacts, on purpose -- a channel identity is a fact, and who is behind it is a judgement.
That judgement lives in Memory: link a contact to a person's card and both channels become one person, with one card, one history, and one set of facts. A contact detail page shows a Memory chip when it is linked, and offers to link it when it is not. Nothing links itself -- inbound messages create contacts and link nobody.
How contacts are created
Contacts are never created by hand-typing a form first. Two paths populate them:
- Backfill. Existing conversation history is distilled once: every distinct (workspace, channel, participant) combination becomes a contact, with its display name and avatar taken from the most recent conversation that had one.
- Auto-upsert on inbound. Every inbound WhatsApp or email message resolves (or creates) the contact for its sender and links the conversation to it. A new message from a known identity refreshes the contact's display name or avatar when the incoming value is different from what's stored -- it never blanks out a known name just because one particular message didn't carry one.
A workspace member can also create or edit a contact manually (display name and notes only -- the channel + identifier pairing that IS the contact's identity is fixed once created).
The /contacts page
A searchable list -- name or identifier, with a channel badge -- and a detail view per contact: identity info, an editable display name, and a read-only, cross-channel timeline of every conversation that contact has had. Each timeline entry links into the real inbox thread. No groups, favorites, or import in v1 -- list, search, detail, on purpose.
Using contacts as an agent: the contacts.* tool family
contacts.search(viaapi_call) -- find contacts by name or identifier, optionally narrowed to one channel.contacts.get-- read one contact by id, or by channel + external_identifier.contacts.upsert-- create a contact for a channel identity, or update its display_name/metadata if one already exists. Never changes which identity a contact represents.needs_approvalisnever: this only writes this workspace's own contact record, with no side effect outside it.