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.

Put your agent in a chat box on your own website with one script tag. The agent serves the box itself: no Flow Messaging, no other service.

Visitors to your site ask the agent what they would ask a person: “is this one still available?”, “when can it arrive?”. They get the same tools, skills and conversation as on WhatsApp, in a box on your page. Add webChat() to the agent; it serves the box and its API under /v1/web-chat.

Requires Flow Framework 0.12.0 or later. To keep your own chat UI and only use Flow’s conversation runtime, see your own chat UI instead.

Add the channel

In src/agent.ts, add webChat() to channels with the sites the box may be on:

src/agent.ts
import { webChat } from "@flow-engineer/flow";
// In buildAgent:
const agent = createAgent({
// ...the starter's options, host: options.host included
channels: [
...unconnected,
...connected,
webChat({ origins: ["https://www.example.com"] }),
terminal.channel,
],
});

webChat() is named web, and two channels cannot share a name. The starter has a placeholder customUI("web", provider) in its unconnected list: delete it from that list, or the agent stops with two channels have the same name. Keep webChat() outside unconnected, which is empty on Flow’s hosting.

The agent’s send tool on this channel is send_web. beforeSend, content rules, traces (channel web) and scenario tests work as on every channel.

Add the script tag to your site

Put one tag on every page that should show the box. The agent serves the script, so it always matches the agent’s version:

Your page
<script async src="https://<agent address>/v1/web-chat/widget.js"
data-title="Ask us"></script>

On Flow’s hosting the agent address is FLOW_AGENT_URL, which npx @flow-engineer/flow deploy status prints.

Allow the agent in your site's CSP

If your site sends a Content Security Policy, add these to the directives it already has:

Content-Security-Policy additions
script-src https://<agent address>;
connect-src https://<agent address> 'self';
img-src https:;
  • script-src: the launcher script and the panel it loads.
  • connect-src: the box’s calls to the agent; 'self' only if you use data-identity-url.
  • img-src: the agent’s photos, carousel pictures and data-avatar. Name their hosts instead of https: if you know them.
  • style-src needs nothing. Safari before 16.4 falls back to a <style> element, which needs 'unsafe-inline'. font-src needs nothing unless you set the box’s font to your own web font.

Sites that enforce Trusted Types (require-trusted-types-for 'script') are not supported yet.

Check it worked: an “Ask us” button appears at the bottom right of the page. Open it and ask “What’s on the menu?”: the agent’s reply appears in the box. Reload the page and open it again: the conversation is still there.

  • A launcher button, then a chat panel on first open (a full-screen sheet on phones). The launcher script is small; the panel loads only when opened.
  • The agent’s messages with Markdown, reply buttons, a list to pick from, a link button, photos and carousels. A tap on a button or a list row is sent as the visitor’s answer.
  • Typing and status lines while the agent works.
  • The same conversation in every tab and after a reload. The box shows each message once, in order, through dropped connections, restarts and deploys (delivery guarantees).
  • English or Hindi for the box’s own words (data-lang, else the page’s language).

Not on the web chat yet: files and photos from visitors, and editing or unsending a message.

AttributeWhat it does
data-titleThe launcher’s and panel’s title. Default “Chat with us”
data-greetingA first message shown before the visitor writes
data-avatarAn https image shown in the panel’s header
data-contextJSON of the page’s details, sent with each message (page context)
data-identity-urlYour same-origin URL that returns a signed-in visitor’s token (signed-in visitors)
data-langen or hi. Default: the page’s lang, else the browser’s
data-themelight, dark or auto (default, follows the system)
data-positionright (default) or left

The page can also drive the box through window.FlowChat:

CallWhat it does
FlowChat.open(), FlowChat.close()Opens or closes the panel
FlowChat.setContext({ ... })Replaces the page details sent with each message from now on
FlowChat.signIn()The visitor signed in on your site: the box starts a signed-in session
FlowChat.signOut()The visitor signed out: the box goes back to an anonymous visitor

To match your brand, set these on the box’s element, flow-chat, in your stylesheet:

Your stylesheet
flow-chat {
--flow-chat-accent: #2457d6; /* the launcher, the visitor's messages, the send button */
--flow-chat-accent-text: #ffffff; /* text on the accent */
--flow-chat-font: Inter, system-ui, sans-serif;
--flow-chat-radius: 16px;
}

By default every visitor is anonymous. The box gets an id that lasts 30 days, renewed while they come back, and keeps it in the browser. The agent knows them as web:visitor:<id> (context.person in tools). Safari deletes a script’s storage after 7 days without a visit; the visitor then starts a new conversation.

When the visitor is signed in on your site, make them the same person the agent knows on other channels, with their whole conversation and every tool. Your backend signs a short identity token; the box exchanges it for a session.

Share a key between your backend and the agent

Make a random key of at least 32 characters and set it as FLOW_WEB_CHAT_IDENTITY_KEY in both places (on Flow’s hosting, a secret; see hosting). For example:

Terminal
openssl rand -base64 32

While you rotate it, give the agent both keys, comma-separated, or pass webChat({ identityKey: [newKey, oldKey] }).

Return a token from a signed-in route

sub is the person’s account key: the one your resolve returns for them on other channels. phone is optional: the number your backend verified is theirs, in E.164. In Node:

Your backend (Express)
import { webChatIdentity } from "@flow-engineer/flow/web-chat";
app.get("/api/flow-chat-identity", requireLogin, (req, res) =>
res.json({
token: webChatIdentity({
sub: req.user.id,
phone: req.user.verifiedPhone, // "+919876543210", or undefined
key: process.env.FLOW_WEB_CHAT_IDENTITY_KEY!,
}),
}),
);

In any other language, sign an HS256 JWT with the same key and these claims:

Identity token claims
{ "sub": "member-7", "phone": "+919876543210", "aud": "flow-web-chat", "iat": 1793174400, "exp": 1793174700 }

sub is 1 to 200 characters. phone, when present, is E.164 (+ and 7 to 15 digits); any other value makes the token invalid. exp is at most 10 minutes after the token is made (webChatIdentity uses 5 by default). The token only starts a session; it is not stored.

Name the route on the script tag

Your page
<script async src="https://<agent address>/v1/web-chat/widget.js"
data-identity-url="/api/flow-chat-identity"></script>

The box calls the URL with the visitor’s cookies, so it must be on the page’s own origin. Call FlowChat.signIn() after your site signs someone in without a page load, and FlowChat.signOut() when they sign out.

Check it worked: sign in on your site and ask the agent something only that account’s tools can answer. In a tool, context.key is the sub you signed and context.person is key:<sub>.

  • A visitor without a valid token, or with no data-identity-url, chats anonymously. An invalid token is never an error in the box: it carries on as an anonymous visitor.
  • An anonymous visitor’s chat stays apart from their account when they sign in.
  • Without FLOW_WEB_CHAT_IDENTITY_KEY (or identityKey) every visitor is anonymous.

With phone in the token, the web chat and the person’s WhatsApp, SMS or iMessage chat from that number stay two conversations, but each turn of one is shown their recent messages in the other. Replies still go out only where the person wrote.

For example, a person asked on WhatsApp (+919876543210) “Is my bid on the PS5 still the highest?” and the agent answered “Yes, your ₹30,000 bid leads.” They then sign in on your site with a token carrying that number and ask in the box “what are my bids?”.

Check it worked: the box answers with the PS5 bid, and nothing new goes to WhatsApp. When they write on WhatsApp next, that turn is shown the web chat too. Put only a number your backend verified in phone, never one typed in the chat: an anonymous visitor who types a number stays apart. One person, several conversations has the rules.

Each message carries the page the visitor wrote from (url, title) and your site’s own details: data-context on the script tag, or FlowChat.setContext() for pages that change without a load.

Product page
<script async src="https://<agent address>/v1/web-chat/widget.js"
data-context='{"listing":"ps5-disc"}'></script>

In this tool, inventory stands for your own backend client:

A tool that reads it
const isAvailable = tool({
name: "is_available",
description: "Whether the listing the person is looking at is still available",
input: z.object({}),
run: async (_input, context) => {
const listing = context.page?.context?.listing;
if (typeof listing !== "string") return "Ask which listing they mean.";
return inventory.status(listing);
},
});
  • The agent reads the page as one line of data in its turn, never as instructions. Tools get it as context.page: { url, title?, context? }, from the visitor’s latest message.
  • The page and the details together are at most 1 KB. To fit, the box drops the page’s title, then your details, then the URL’s query string.
  • With origins set, a message whose url is not on one of your sites is refused.
  • It comes from the visitor’s browser: treat it as a hint, never as proof of who they are or what they may see. Check access in your tool against context.key.

The box is public: anyone can open your page, and a script can call the agent directly. Each layer stops something different.

LayerWhat it stopsWhat it does not stop
Allowed sites (origins)Other websites putting your agent on their pagesA script or server calling the API directly: it can send any Origin
SessionsCalls without a session: every call but starting one needs itA script that starts sessions like a browser
LimitsScripts and floods running up your model spendAbuse within the limits

origins lists where the box may be: https://www.example.com, or https://*.example.com for any subdomain (not the bare domain, which you list separately). http:// is allowed only for localhost and 127.0.0.1. FLOW_WEB_CHAT_ORIGINS, comma-separated, adds to the list.

With no sites listed at all, any website may embed the agent and spend its credit. Use that only for development: Flow’s hosting refuses it in production (web_chat_open).

The box starts a session that lasts 1 hour and renews it before it ends. Sessions and anonymous ids are signed with sessionKey (default FLOW_WEB_CHAT_SESSION_KEY, at least 32 characters). Flow’s hosting sets it for you. Without one, the agent makes a random key per process, and every session and anonymous visitor ends when the process restarts.

Set any of these in webChat({ limits }):

WebChatLimitsDefault
newVisitorsPerMinute, newVisitorsPerDay20 and 300 new visitors per network address. Mobile networks put many people behind one address
messagesPerMinute, messagesPerDay8 and 60 per anonymous visitor
signedInMessagesPerMinute, signedInMessagesPerDay15 and 300 per signed-in visitor
messagesPerAddressPerMinute60 per network address, all its visitors together
messageChars2,000 characters a message (request bodies at most 16 KB)
turnsAtOnce20 web chat turns at once; more wait their turn, with typing shown
  • Wrong identity tokens are refused after 10 from one address in 15 minutes.
  • A visitor over a limit sees “Too many messages. Try again in a while.” in the box.
  • Limits are held in the process’s memory, so with several processes each counts its own. Flow’s hosting runs one process per environment, so its limits are exact.
  • There is no cap on the agent’s web chat messages a day. The workspace’s model credit limits still apply, and an agent-wide daily cap may come in a later release.
  • Logs carry ids and counts, never message text, network addresses or tokens.

The limits per network address need the visitor’s real address. Behind a load balancer or reverse proxy, the agent sees the proxy’s address unless you say how many proxies append to X-Forwarded-For:

src/agent.ts — the channel
webChat({ origins: ["https://www.example.com"], trustProxy: 1 });

or FLOW_TRUST_PROXY=1. The client’s address is that many entries from the end of X-Forwarded-For; entries before it are what the client sent and are ignored. Set it to exactly the number of proxies you run: a higher number lets a client choose its own address. Without it, the limits per network address are off (the agent logs a warning once); the limits per visitor still hold. Flow’s hosting sets it for you.

The playground plays webChat() as its web simulator, so test people can use it like any other channel. To try the real box on a page:

Make a test page

In a folder outside your agent, say site/index.html:

site/index.html
<!doctype html>
<html lang="en">
<title>Web chat test</title>
<h1>Our café</h1>
<script async src="http://localhost:8788/v1/web-chat/widget.js"
data-title="Ask us" data-context='{"page":"home"}'></script>
</html>

Use the port npm start prints if it is not 8788.

Allow the test page and start the agent

Terminal
FLOW_WEB_CHAT_ORIGINS=http://localhost:5500 npm start

Then, in a second terminal, serve the page:

Terminal
npx serve -l 5500 site

Setting the local site on the command line keeps it out of .env, which deploy sends as secrets.

Chat

Open http://localhost:5500, open the box and ask “What’s on the menu?”.

Check it worked: the reply appears in the box, and after a reload the conversation is still there. The same page served on any other port gets no box: its calls are refused with 403 origin_not_allowed.

Locally, without FLOW_WEB_CHAT_SESSION_KEY, a restart of npm start ends the box’s session and anonymous id: the box starts a new conversation.

  • Set for you: FLOW_WEB_CHAT_SESSION_KEY (one per environment, kept across deploys, so sessions survive them) and FLOW_TRUST_PROXY=1. deploy never sends either from your .env.

  • You set: your sites, in code or as the secret FLOW_WEB_CHAT_ORIGINS, and for signed-in visitors the secret FLOW_WEB_CHAT_IDENTITY_KEY.

  • Production refuses a web chat open to any site (web_chat_open); staging deploys it with a warning. Promoting such a staging version is refused too while production has no FLOW_WEB_CHAT_ORIGINS. Fix it by listing your sites in webChat({ origins }), or set the secret and deploy again:

    Terminal
    printf '%s' 'https://www.example.com' | npx @flow-engineer/flow deploy secret FLOW_WEB_CHAT_ORIGINS --env production

    A secret takes effect on the next deploy.

  • The box’s routes are served at FLOW_AGENT_URL + /v1/web-chat (routes).

The agent’s store must be on Flow Framework 0.12 for an agent that uses 0.12: until Flow updates it, the deploy is refused with chinwag_too_new.

  • Set FLOW_WEB_CHAT_SESSION_KEY (or sessionKey) to a fixed random key of at least 32 characters, the same in every process, so sessions survive restarts and deploys.
  • Set trustProxy or FLOW_TRUST_PROXY to the number of proxies in front of the agent.
  • Serve agent.fetch(request) for /v1/web-chat and everything under it. Allow streaming responses (server-sent events, up to 14 minutes) and do not buffer them.
  • Use a durable database (sqlite(file) or postgres(pool)). The box’s messages wait in an outbox in the store; with the default in-memory store they are lost at a restart.
  • A store of your own that forwards Store calls must run Store migration 12, serve its six new methods and pass addIdentity’s verified number on (see the framework’s 0.12.0 release notes).
webChat() optionWhat it does
originsThe sites the box may be on. Joined with FLOW_WEB_CHAT_ORIGINS
identityKeyThe key, or keys while rotating, your backend signs identity tokens with. Default FLOW_WEB_CHAT_IDENTITY_KEY (comma-separated)
sessionKeySigns sessions and anonymous ids. Default FLOW_WEB_CHAT_SESSION_KEY
trustProxyHow many proxies append to X-Forwarded-For. Default FLOW_TRUST_PROXY
limitsWebChatLimits, above
name, baseThe channel’s name (default web, send tool send_<name>) and where its routes answer (default /v1/web-chat)
info, instructionsWhat your box can show, over WEB_CHAT (narrow it for a box of your own); your guidance for the agent on this channel