Skip to content

Instant messaging

Instant messaging pulls every chat app you use into one page: Slack and Telegram over their real APIs, WhatsApp, Messenger, and LinkedIn through the browser extension, since none of them offer a usable API for this. Every inbound message becomes an event, flows through triage like mail and calendar do, and can land a row in your task queue with a reply box right there.

Every messaging app implements the same small seam: {fetch, send, capabilities}.

  • fetch(since?) drains everything new since a watermark and returns it as a normalized {conversationId, conversationTitle?, providerMessageId, ts, sender, body, meta?} list.
  • send(conversationId, body, opts?) posts a reply; opts.claimKey lets a connector de-duplicate a retried or double-fired send so it never queues the same message twice.
  • capabilities reports whether the connector can send at all and the latency you should expect, shown in the UI so you know what you’re waiting on.

Slack and Telegram connect directly:

  • Slack rides @slack/web-api for sends and Socket Mode for a live inbound feed (buffered inside the connector, drained on the same poll as everything else).
  • Telegram rides GramJS, a full MTProto client, so it behaves like your own Telegram session rather than a bot account.

Both are polled every 30 seconds by the IM sync engine, same cadence pattern as Mail.

WhatsApp, Messenger, and LinkedIn have no usable API, so they ride the Chrome extension’s job queue instead, running in your own logged-in browser session:

  • Fetching happens every two to five minutes: the server enqueues one fetch job per site (skipping a site that already has one queued or running) and the extension drains it, delivering whatever it found back through the same inbound pipeline as the API connectors.
  • Sending enqueues a job the extension executes the next time it’s active; the UI shows it as queued, sends via browser rather than sent until that happens.
  • A connector that finds itself logged out on the target site files a task-queue card asking you to log back in, background fetch can’t recover from that on its own.

/p/im opens on a Unified tab (no filter, active by default) plus one tab per configured connector. Each app tab shows an unread count as a red ·N badge, and browser-automation apps carry a faint via browser annotation so you know their latency differs from Slack or Telegram. Selecting an app tab (not Unified) marks it seen, clearing its badge; the Unified tab never marks anything seen on its own.

Each message row shows an initials avatar, the sender, the conversation title and time, and the body. A row whose message already has an open task-queue entry carries a small also in task queue tag so you’re not tempted to act on it twice.

  • Inline reply: every row has a reply affordance that reveals a text field, Enter sends, Escape cancels. API-connector sends show sent immediately; browser-connector sends show queued, sends via browser until the extension delivers it.
  • → inbox: routes a message into the capture inbox as “<sender>: <body>” for later triage as a task, independent of replying to it.
  • The page respects the same not-now gate as Mail: while locked it shows the shared not-now screen, and an active frequency restriction shows a small viewable every Nh chip in the header.

Every inbound IM message becomes an ops.events row the same way inbound mail does, which triage classifies and, depending on severity, surfaces as a task queue row with the same inline reply affordance the IM page itself offers.

Agents reach IM through a gated tool, im.send {app, conversationId, body}, requiring approval before it fires. It resolves to the matching connector and sends through the same send() seam the UI uses, claim-guarded per call so a retried tool invocation can’t send twice. It’s the tool the outreach sequence workflow uses for its IM touches, alongside gated mail sends.