Skip to main content
An agent that works in a demo meets new problems the day real people write to it. None of them are about the model. Each section below says what happens, what Flow Messaging does for you, and what you must do yourself if your agent talks through another channel. You may not message just anyone. In the US, a business may text first only someone who agreed to it, and anyone can text STOP to stop all messages. WhatsApp needs the person’s opt-in, and lets you write freely only within 24 hours of their last message. On Telegram and iMessage the person writes first. People also write STOP, “stop texting me” or “unsubscribe” in the middle of a conversation. Your agent must not answer a STOP with a cheerful reply.
  • On Flow Messaging: the send gate applies each channel’s rule to every send. On SMS, a start needs the contact’s recorded consent unless they texted first; STOP (and STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, REVOKE, OPT OUT, OPTOUT) opts the contact out of all your SMS numbers until they text START, and Flow sends the keyword reply itself. You still receive the message; a send to an opted-out contact answers 403 permission with contact_opted_out. Do not retry it.
  • On another channel: your SMS provider may block sends after STOP, but check what it does and which words it treats as STOP. Record consent before you text anyone first. Make your agent recognise STOP and its variants, send nothing more, and record the opt-out where every part of your system that sends can see it.

Quiet hours

A text at 3 a.m. wakes people up. Messages you start (reminders, follow-ups) should wait for the daytime in the person’s time zone. A reply to someone who just wrote to you is fine at any hour.
  • On Flow Messaging: on SMS, a send to a contact who has not texted in the last hour respects the sender’s quiet hours (by default 21:00 to 08:00 in the contact’s time zone, worked out from the area code). Inside them it answers 409 outside_window with quiet_hours and retry_after, and a queued message is held until morning instead of sent at night.
  • On another channel: keep a time zone per person (from their area code, or ask), and check it in every job that sends without being asked. Check the rules that apply to your messages and your users.

Message length and splitting

A long, formatted answer reads well in a web chat and badly on a phone. One SMS holds 160 plain characters, or 70 if the text has any emoji or non-Latin letter; a longer text goes as several segments, each billed, and some phones show them out of order. Markdown is shown as raw asterisks and pound signs on SMS. On chat apps, one wall of text is harder to read than two or three short messages.
  • On Flow Messaging: reply() splits your model’s text into chat-sized bubbles at paragraph and sentence ends, and turns Markdown into plain text on channels without formatting. See Streaming replies. On SMS a text is capped at 1530 characters and 10 segments.
  • On another channel: ask the model for short answers, strip Markdown for SMS, and split long answers at paragraph or sentence ends yourself (the bubble rule works for any channel). If you send Observe traces, report each part you actually sent as its own flow.agent_reply.

Duplicates and retries

The same message can reach your code twice: webhooks are delivered at least once, providers retry when your server answers slowly, and people send the same thing again when no reply comes. Your send can be retried too. Without care, the person gets two answers, or the same answer twice.
  • On Flow Messaging: deduplicate on the event’s id (see delivery rules), and send every reply with an Idempotency-Key built from it, so a retried send never sends twice. See Idempotency.
  • On another channel: deduplicate on your provider’s message id before you run the agent, and use your provider’s idempotency feature on sends if it has one. Answer your provider’s webhook at once and run the agent afterwards, so a slow model does not trigger a retry. Set flow.message_id on your traces, so Observe stores the person’s message once even when your code ran twice (it still records every reply you actually sent).

People who come back days later

People answer a question three days later with “yes”, or start a new topic in an old thread. Your agent needs the history to know what “yes” means, and it must not treat old context as current (“your order is on its way” from last week). Observe counts a conversation as one run of messages: after 48 hours with no messages, the next one starts a new conversation. Billing counts the same way.
  • On Flow Messaging: one person on one sender is one conversation object, however long the gap. Load the recent history with GET /v1/conversations/{conversation_id}/messages, and check message times when context may be stale. On WhatsApp, more than 24 hours after the person’s last message you can send only a template.
  • On another channel: keep your own history per thread (for SMS, per pair of your number and theirs), with times. Send the same flow.conversation_id for the whole thread; Observe splits it at 48-hour gaps itself.

Photos, voice notes and other media

People send photos, screenshots, voice notes, stickers, contact cards and locations, often with no text at all. An agent that ignores them answers as if nothing was said. Media can also be sensitive: a photo of a prescription or an ID card.
  • On Flow Messaging: every message has a typed content type. contentText() gives you the text, a caption, a voice note’s transcript or a button’s label. When your reply holds something a channel cannot show, fallback: "auto" sends the closest thing and reports what was shown.
  • On another channel: decide what your agent does with each kind: read it, ask for text, or say it cannot open it. Never write media or its links to your logs. On the traces route, Observe grades the text of each turn; a message with only a photo has no text to grade.

Latency and typing

On a phone, ten seconds of silence feels broken. People write again (see duplicates, above) or leave. Tools and long model calls are the usual cause.
  • On Flow Messaging: reply() keeps the typing indicator on while your model writes and sends each bubble as soon as it is ready. For a slow tool call, wrap the work in event.conversation.responding(...). Webhooks must answer within 10 seconds: run the agent after answering, or read events from the stream. See Streaming replies.
  • On another channel: SMS has no typing indicator. For work that takes more than a few seconds, send a short “Looking that up” first, and keep the final answer short.
Observe shows your reply time per conversation, and its median and 95th percentile, so you see slow turns before people complain.

Replies that are wrong and nobody reports

Most bad replies are never reported. A confident wrong price, a made-up policy, an answer to a different question, a missed request to talk to a person, a reply that ignores that someone is in danger: people act on them, or quietly leave. Thumbs-down buttons catch a tiny share, and only from the people who still care. This is why Observe grades every reply, not a sample, and groups failures into issues by how many conversations they touch: the problems that reach the most people come first, each with the replies that show it. See What Observe gives you.

Test before you ship

Every prompt or model change is a new agent. Keep a set of test chats, including the ones that failed before, and run them against each new version before people see it. On Flow Messaging, simulated contacts play the person’s side without a phone; on any channel, test conversations are graded in Observe. See Test before you ship.