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.

Run the same agent on WhatsApp, iMessage, Telegram, SMS, your website and your own app.

Let someone text your product the way they’d text a person: a short request, another detail, then “actually, make that tomorrow.” They shouldn’t have to learn a command language or wait for a slow lookup before they can say anything else.

Flow runs that conversation: it reads incoming messages, calls your tools and replies using each channel’s formatting and delivery rules. Its turn handling accounts for double texts, corrections and work that takes longer than a quick reply. Connect through Flow Messaging or bring your own provider.

Connect a channel

From the starter project, run:

Terminal window
npx @flow-engineer/flow link --channel telegram

link signs you in, sets up the folder and prints a phone link. If a channel is not open yet, it tells you which ones are available.

Start the agent

Terminal
npm start

Restart after link changes keys, senders or the display name. The starter reads .env at startup and connects through an outbound event stream, so your laptop needs no public URL or tunnel.

Send the first message

Open the phone link and write to the agent. WhatsApp and iMessage trial channels reply after you write first.

Check it worked: ask “What’s on the menu?” and look for the get_menu call in the local playground’s Debug view.

The trial uses shared test numbers. A join code binds your phone to your app; another app’s code moves it. Use your own channel for real customer data.

InterfaceConnect with
Telegram and iMessageFlow Messaging’s channel definitions
WhatsAppFlow Messaging or whatsapp() with your provider
SMSFlow Messaging’s SMS channel or sms() with your provider
A chat box on your websitewebChat(), served by the agent itself (web chat)
Your own web or app chat UIcustomUI() with your sending function

The Flow Messaging adapter covers its senders and connection setup.

Adapt your own provider

Define the channel using callbacks to your provider. In this fragment, replace waClient and smsClient with the API clients you use:

Channel definitions
import { sms, whatsapp } from "@flow-engineer/flow";
const wa = whatsapp({
sendMessage: (to, message) => waClient.send(to, message),
react: (to, messageId, emoji) => waClient.react(to, messageId, emoji),
});
const text = sms({
sendMessage: (to, message) => smsClient.send(to, message.text),
});

Pass these definitions in createAgent({ channels: [wa, text], ... }). Your webhook adapter supplies incoming messages with agent.message({ channel, from, message, messageId }).

Providers can also expose carousels, typing, read receipts and other optional capabilities. Flow offers the model only the features the connected provider supports.

Quick messages fold into one turn; a new message during a tool call lets the agent respond while that tool finishes in the background.
What happensHow Flow responds
Several quick messages arriveFolds them together; an in-flight model step restarts with the new context
A tool takes timeSends an acknowledgement with the tool call and waits for the result
The person writes during a tool callReads the message at once; the tool continues in the background
A tool passes its deadlineMoves it to the background; the default is 60 seconds
The model writes without using a send toolReminds it once to send; if it writes plain text again, sends that text as the reply and logs a warning; does not repeat text already delivered

The agent gets a send tool for each channel and answers where the person last wrote. The playground lets you exercise this behavior without sending to a real provider.

WhatsApp can use buttons, lists, replies, reactions and declared templates. SMS uses plain text segments. Your custom UI can define additional fields.

Content is checked before sending. If a message does not fit the platform’s limits, the agent receives the refusal and can send again. The playground and hosted tests apply the same limits.

Use labels for simple buttons, or { id, label } when taps must be distinguished.

Button values
buttons: [
{ id: "details:latte", label: "Details" },
{ id: "details:chai", label: "Details" },
],

A tap is delivered as choice: { id, title }. Ids are 1–64 bytes and must be unique within the message and across carousel cards. Labels can repeat. Check the channel’s info.buttonIds before relying on ids: some providers return only a label.

Use beforeSend for your own final-message checks. Its replacement is what gets validated, sent and recorded. Hook reference

From Framework 0.12.0, the agent’s prompt carries three lines on how its messages read, on every channel: one idea per message or short paragraph, catalogue or tool items named in plain words (“an oat latte”, not “LATTE OAT LG 16OZ”), and, on channels with buttons or lists, buttons only when the person’s next step is to pick one of a few options.

  • A channel’s instructions add to it for that channel.
  • createAgent({ writing: "..." }) replaces all three lines with your own text.
  • createAgent({ writing: false }) leaves them out.

To keep the button wording from before 0.12.0, pass writing: "When they choose between a few options, offer them as buttons so they can tap."

SMS and WhatsApp from the same phone, and an app identity resolved to the same account, reach one conversation.

SMS and WhatsApp numbers are normalized to E.164. For example, +1 555 010 0100 and 15550100100 identify the same phone.

For other interfaces, supply resolve to map the verified channel identity to your account key. The tool receives that key as context.key and a stable identity as context.person.

National phone numbers and identity formats

Set defaultCountryCode: "+44" if a provider supplies national numbers such as 020 7946 0018. On phone channels other than WhatsApp, the configured country code is applied and the national trunk zero is dropped. Without it, bare digits are read as international.

context.person is key:<resolved key>, else phone:<E.164>, else <channel>:<address>. context.key is the exact resolved account key or null. Flow does not guess that an app user and a phone number are the same account; a verified number only links their conversations (below).

Requires Flow Framework 0.12.0 or later. Identities with the same key are one person in one conversation. A person can also have conversations under different keys: WhatsApp as phone:+919876543210, and your app or web chat signed in as key:member-7. When both carry the same verified number, they stay separate, but each turn of one is shown the person’s recent messages in the others, and the agent’s to them: the last 20 messages of the last 7 days, oldest first, each with its channel and time, as data. Each reply still goes out on the channel the message came in on.

A number links conversations only when it was verified:

  • a phone channel’s own number (WhatsApp, SMS, iMessage);
  • the phone claim of a signed-in web chat visitor’s identity token;
  • phone passed to agent.message() from your own channel code, in E.164: your app’s signed-in user, or, on a Telegram integration of your own, a contact the person shared whose user id is the sender’s. Flow Messaging’s channels pass none beyond the phone channels’ own.

Never pass a number someone typed: an anonymous visitor stays apart whatever they write. In this fragment, user is the account your backend verified for an incoming message on your own chat UI:

Your backend, for a signed-in user
await agent.message({
channel: "app",
from: user.id,
key: user.id,
phone: user.verifiedPhone, // E.164, "+919876543210"; agent.message throws on any other form
message: text,
messageId,
});

Check it worked: the person asked on WhatsApp “Is my bid on the PS5 still the highest?” and was told “Yes, your ₹30,000 bid leads.” In your app they ask “what are my bids?”: the reply names the ₹30,000 PS5 bid and goes to the app only. That turn’s prompt holds the WhatsApp chat as data, in the person’s time zone, else the agent’s timeZone, else UTC:

The turn's prompt, in part
- whatsapp, Tue 27 Oct 14:02, they: "Is my bid on the PS5 still the highest?"
- whatsapp, Tue 27 Oct 14:02, you: "Yes, your ₹30,000 bid leads."
  • One agent only. Conversations are linked within one agent, never across agents, also when several agents share a database.
  • Forgetting reaches every linked conversation. agent.forgetPerson(conversationId) deletes the person’s conversation and every conversation that shares a verified number with it.
  • Identities first seen before Framework 0.12.0 get their number from their next message on.

Keep account lookups in profile. Flow exposes them through get_user_profile, which the agent calls when needed.

In this configuration fragment, members is your application’s account service:

Account profile
resolve: ({ channel, address }) => members.idFor(channel, address),
profile: async ({ person }) => members.profile(person),

A tool you define with the same get_user_profile name takes precedence. In scenario tests, the profile is simulated from the test world rather than read from your real backend.

Use context(conversationId) only for small facts at hand: it runs before every turn, so a slow lookup there delays every reply. Shared facts such as opening hours and policies can live in the agent’s instructions.

The agent sees the person’s time when their zone is known. Otherwise it sees the business’s timeZone, marked as the business’s zone. It never assumes the two are the same.

Set a known person’s zone from your backend:

Set a time zone
await agent.setTimeZone({
to: { channel: "sms", address: "+15550100100" },
timeZone: "Asia/Kolkata",
});

This returns { status: "set" }, or { status: "refused", reason: "unknown_person" } for a person who has not written yet. An invalid IANA zone throws.

The person can also name their zone in the conversation. A reminder “at 9 tomorrow” requires one; “in 30 minutes” does not. A no-contact rule “until Friday” uses the latest possible end of Friday, UTC−12, so it does not lift early.