@runflow-ai/sdk/conversation) lets an agent operate inside the Conversation Hub — Runflow’s WhatsApp/human-in-the-loop console. Answer the conversation that triggered the agent, show a typing indicator, send templates and media, open a ticket for a human attendant, and keep the contact’s profile (tags, internal notes) up to date.
Requires the Conversation Hub feature enabled for your tenant. All calls are authenticated with a tenant-scoped API key; conversations from other tenants return 404 (existence is never leaked).
Setup
One click: with the feature enabled, open Credentials in the Runflow portal, pick Conversation Hub in the credential type dropdown and hit Generate key. The key is minted by the Conversation Hub, stored encrypted as theconversation-api-key credential, and never shown in the browser. Re-running it rotates the key (the previous one is revoked instantly).
That’s it — the SDK resolves the key from the credentials store on the first call and talks to the production Conversation Hub by default. Just import the default instance:
Getting the conversation
fromTrigger
The conversation that activated this agent — the standard path for triggered agents.
byId
Lazy handle for a known conversation id.
byPhone
Proactive sends — resolve (or create) the thread from a phone number.
byPhone matches every stored phone variant (with/without +, with/without the Brazilian mobile 9).Inbound media
Every inbound media message (audio, image, video, document, sticker) reaches the agent after the Hub downloads it, with a presigned URL (6h TTL) patched into the Meta-shaped envelope —messages[0].audio.link, messages[0].image.link, and so on. Captions are optional; a bare image triggers the agent the same way. Don’t use the accompanying media id: it is Meta’s single-use media id and the Hub’s own download has already consumed it — always read link. Voice notes additionally carry the URL as the trigger’s message body and in metadata.audioUrl, ready for transcription.
Sending messages
Interactive messages
sendInteractive covers every interactive type WhatsApp supports. button and list are also composable by attendants in the inbox; the rest are agent-only, because they need ids that live outside the portal (published Flows, catalogs, payment configurations).
A body over Meta’s 1024-char limit is not rejected: the Hub sends the excess as leading text bubbles and returns their ids in
leadMessageIds, keeping the tail right above the buttons. You never have to pre-chunk.metaMessageId — the wamid an inbound tap, submission or payment will reference through context.id.
Flow (native form)
flowToken is yours. It travels to Meta untouched and comes back inside the submission (nfm_reply → response_json), so it is what correlates an answer with the context that asked for it — an order id, a checkout attempt, a profile edit.
screen picks the entry point: with it the message navigates to that screen and data prefills it; without it the Flow resolves its first screen on your Flow endpoint (data_exchange), and sending data is an error. Use mode: 'draft' to render a Flow that is not published yet.
Carousel
Payments (order_details / order_status)
Every amount is an integer in cents, andtotalAmount has to equal subtotal + tax + shipping − discount — the Hub rejects the send with a 400 when it doesn’t, which reads far better than Meta’s own error.
Who owns the charge is a choice — exactly one of the two, never both:
paymentConfiguration— a payment configuration registered on your WABA; Meta/your PSP generates the charge.pixDynamicCode— an inline PIX charge your backend generated ({ code, merchantName, key, keyType }, wherecodeis the EMV “copia e cola” payload). Use it when the charge id, expiry and reconciliation live on your side.
Safe retries (idempotency)
Every send accepts anidempotencyKey. Retrying with the same key returns the original message instead of double-sending — the reply carries deduplicated: true.
Typing indicator & read receipts
typing() right when the trigger fires, before slow work.
Handoff & resolution
transfer opens a ticket in the attendants’ queue and flips the conversation out of AI mode. resolve closes the case.
Choosing where to hand off
conversation.directory is a read-only view of your workspace’s service structure — the departments and the attendants (human agents) registered in the Conversation Hub. Use it when the agent needs to decide the transfer target at runtime instead of hard-coding a department name.
status (presence: online / busy / away / offline) and load (how many human conversations they are handling at this moment). A common pattern — route to the least-busy online attendant, falling back to the department queue when nobody is available:
Legacy sync agents
Agents invoked through the direct execution endpoint (no HTTP trigger) answer with thereply helper — intent drives the handoff:
Contacts, tags & notes
Agent-authored notes have no human author — the portal shows the
agentName label (default: Agente AI).Custom fields (metadata)
Contacts carry a flatmetadata object — key/value pairs for whatever your domain needs (CPF, birth date, plan, consent flags). Both create and update accept it:
- A write only touches the keys it mentions; every other key stays as-is.
- Sending
nullfor a key removes it. - On
createwithupsert: true, metadata is merged into the existing contact’s metadata (it never wipes what’s already there).
contact.metadata comes back on every read that returns a contact — contacts.getByPhone and conv.get().contact. The value type is exported as ContactMetadata from @runflow-ai/sdk/conversation (re-exported from the SDK root as ConversationContactMetadata).
Metadata is strictly per contact — there is no shared field schema. A key written on one contact exists only on that contact; the portal renders whatever keys each contact carries, and any key matching the rules above is accepted.
Metadata in the inbox
The fields your agent writes are not just storage — they surface in the attendants’ console. Each contact’s keys render on their profile and on the inbox side panel, and the inbox can filter conversations by connection (which WhatsApp number the thread lives on), by agent, and by custom field — the attendant types any key and value, and the filter does a case-insensitive partial match on that key’s value. Pull up, say, every conversation whose contact hasplano containing premium on the sales number.
These filters are a portal feature. The agent API does not expose filtered conversation listing — from agent code you reach conversations through fromTrigger, byId and byPhone.
Error handling
API failures throwConversationApiError with status, a typed code when available, and the raw body:
OUTSIDE_24H_WINDOW (free-form send outside Meta’s window), CONTACT_NOT_FOUND / CONVERSATION_NOT_FOUND (byPhone without create), CONNECTION_NOT_FOUND (unknown connectionId).
Status callbacks
Meta reports what happened to every message you sent —delivered, read, failed. The Hub always consumes those: the ticks move in the inbox and the message row gets its status either way.
Forwarding them to the agent is opt-in, because each callback becomes one agent execution — a 1,000-message campaign can mean a few thousand runs. Turn it on only for flows that actually track delivery (a reminder that escalates when it goes unread, for instance).
In the Runflow portal: Agents → configure the agent → Encaminhar entregas/leituras, where each agent inherits the organization default or overrides it. The organization-wide value lives in Settings → Recursos (admins only).
metadata.kind = 'status' and the raw Meta status payload — the same wamid your send returned as metaMessageId.