Skip to content

Ask the docs

Answers come only from these docs and are written by an AI model (Gemini), so check the linked pages. Your question is not kept. Do not include personal details or keys.

What Flow Framework promises about the messages an agent sends and receives: what a send means, ordering, duplicates, and what your own provider must do.

What Flow Framework promises about an agent’s messages on every channel, and what it does not. Each promise is checked against every channel adapter the framework ships by its conformance suite, before every release.

A channel’s send resolves once the platform has accepted the message, and rejects when the platform refused it, with the platform’s reason. Accepted means the platform took it to deliver:

ChannelResolves when
Flow Messaging (WhatsApp, SMS, Telegram, iMessage)Flow reports message.sent: the channel itself took it, not only Flow’s 202, which means queued. waitFor: "delivered" waits for message.delivered instead, where the channel reports it
whatsapp(provider), sms(provider), customUI(name, provider)Your provider’s sendMessage (or sendCarousel, sendTemplate) resolves. Write it to resolve when your platform’s API accepted the message, not before
webChat()The visitor’s outbox in the agent’s store kept it, under the visitor’s next number: the chat box reads it from there. It rejects when that write fails
The playground and the test cloudThe simulator has it

A send may resolve with how far the message got: sent, delivered, queued (a provider that does not wait, such as Flow Messaging with waitFor: "queued"), or unconfirmed. unconfirmed means no signal came within the provider’s wait (30 seconds by default for Flow Messaging): the message may still arrive and is never sent again. The agent emits message.slow and carries on.

What follows from it:

  • A send tool’s result tells the agent delivered only once the platform accepted the message.
  • message.out is emitted, and the message kept in the history, only then.
  • A rejected send comes back to the agent once, as a “Not delivered” note with the reason (message.undelivered). So does a failure Flow Messaging reports after the send resolved.
  • A carousel the channel sends card by card resolves once the platform accepted its last card.
  • An edit or unsend resolves once the platform accepted the call, and rejects when it refuses it.
  • Typing indicators, read receipts and status lines are courtesies: their failures never fail a turn. Typing is kept up while the agent works and never outlives the turn.
  • One conversation, one message at a time. An agent’s messages to one person go to the channel in the order it wrote them, and each goes only once the one before it was accepted. Messages the framework sends itself (an acknowledgement, a scheduled message, a nudge) wait for the turn in progress and follow the same rule.
  • Inbound, per conversation. A conversation runs one turn at a time and takes the person’s messages in the order the channel gave them; double texts fold into the turn in progress. Flow Messaging delivers events in order per conversation, by stream and webhook alike.
  • Across conversations, nothing is ordered. Different people’s conversations run side by side.
  • Across one person’s channels, each channel is ordered on its own; the agent is told which channel each message came on.
  • A person’s other conversations (under another key, sharing a verified phone number: a WhatsApp chat and a web chat signed in with that number) are shown to each turn as their recent messages, read from the store when the turn starts: a message the other conversation is taking at that moment may show only from the next turn, and a turn whose read fails goes on without them (a warning is logged). Replies go only on the channel of the conversation whose turn it is. Only one agent’s conversations are ever linked, and forgetPerson deletes all of them (one person, several conversations).
  • What a platform does after accepting a message is the platform’s. Flow Messaging sends one message at a time per conversation, in the order accepted; Telegram and WhatsApp deliver in that order in practice but promise nothing more.

Platforms deliver at least once: a webhook is retried until it is answered, and Flow Messaging redelivers events it is not sure arrived. The agent takes each message once:

  • A message whose id the conversation has already taken is a message.duplicate and starts no turn. With a database (sqlite(file), postgres(...)) that holds across restarts and processes; with the default in-memory store, until the process stops.
  • Flow Messaging events already handled are dropped, so a redelivery, or the same event by both the stream and a webhook, reaches the agent once. A failure to handle an event answers the webhook 500 (or does not move the stream’s cursor), so Flow delivers it again.
  • The web chat’s box sends each message with its own id, the same on every retry, so a retry is a message.duplicate. POST /messages answers 202 only once the store has the message; a write that failed answers 503, and the box sends it again.

Outbound: when exactly once is true, and when it is not

Section titled “Outbound: when exactly once is true, and when it is not”
  • Messages the business starts (a nudge, a scheduled message, a template) carry an idempotency key made from their id, the same in every process. A provider that honours keys, as Flow Messaging does for 24 hours, sends each once, even when two processes or a restart send it again. A nudge already settled in the store is never sent again.
  • Retries of one send (Flow Messaging’s on a 429 or 5xx) repeat the same key: never a second message.
  • The agent’s replies are sent once: a send that was accepted is in the history and never sent again, also after a restart. One case is at least once: a process that stops after the platform accepted a reply but before the store recorded it runs the turn again, and the agent may say it again.
  • A send that timed out (unconfirmed) is never sent again: at most once from the framework. The platform may still deliver it.
  • Flow Messaging’s own resend. When a channel took a message but never answered, Flow resends it once, so a rare duplicate is possible on Telegram; an outcome still unknown fails with channel_timeout_unknown_outcome (it may have arrived). Idempotency keys cannot undo that.
  • One process works on a conversation at a time (a lease in the database), so a deploy’s overlap never answers one message twice.
  • Scheduled messages and wake-ups are rows in the store. When one comes due, one process claims it and starts its turn in one transaction, so it runs once, also with several processes or across a restart. A scheduled message found more than 30 minutes late is missed, never sent late; a wake-up found late still runs, and the agent is told when it was due.

The web chat shows each message once, in order.

  • What the box shows waits in the visitor’s outbox in the agent’s store, numbered per visitor. Their tabs and devices share it, and their own messages are echoed into it.
  • The box asks for everything after the last number it showed, and ignores a number it already showed. A phone that slept, a dropped connection, a restart or a deploy loses nothing and repeats nothing.
  • A request waiting on one process wakes when that process keeps an event, and checks the store every 2 seconds for what another process kept (a deploy’s overlap).
  • The outbox keeps a week; a box asking for older reads the history again.
  • Typing and status lines are live only: one sent while no box is connected is lost.

whatsapp(provider), sms(provider) and customUI(name, provider) keep these guarantees only if the provider does its part:

  • Resolve sendMessage when your platform’s API has accepted the message, and reject with a readable reason when it refused it. Never resolve early to go faster: the next message would overtake it, and the agent would be told it was delivered.
  • Pass idempotencyKey on to your platform when it takes one.
  • If your platform’s acceptance can take long, wait a bounded time and resolve { status: "unconfirmed" } past it, rather than hanging the conversation.

To check an adapter of your own, add it to the framework’s conformance suite (conformance/, npm run test:conformance).