Web Apps
How to preview an agent's web app, place it on a Salesforce page, and embed it on your own website.
Overview
A web app is a third way to reach an agent, alongside the Chatbot and the Daemon. Instead of a chat window, the agent gets a page: a form, a dashboard, a wizard, a calculator, whatever the work needs. The page is designed for one agent, stored with it, and served by the Agent Platform already wired to that agent.
The difference from the Chatbot is what the visitor does. A chatbot is a conversation, and everything happens by typing. A web app is an interface, and the agent sits behind the buttons. Someone filling in an intake form works through the form rather than describing what they want in prose.
Everything the agent already has applies: its instructions, its tools, its knowledge, its safety screening, and its memory. A web app is a different front door onto the same agent, not a separate one.
You reach a web app in three places:
| Where | What it is for |
|---|---|
| Preview, in the Agent Builder | Checking the page while you work on the agent. |
| A Salesforce page | Staff and Experience Cloud visitors, using the Web App component. |
| Your own website | Anyone, using a script tag. |
Which web app a visitor gets
An agent can hold more than one web app, each under a short name, and every version of the agent decides which ones it carries.
Visitors always get the deployed version's page. Publishing a page into a draft changes nothing for anyone until that version is deployed, and deploying an older version rolls its web app back with it. Someone who already has the page open keeps the page they loaded; it does not change under them because you saved.
Names become part of a URL, so they are checked as one: 1 to 64 characters, lowercase letters, digits, and hyphens between them, such as intake-form or dashboard2. No spaces, dots, slashes, underscores, or capitals. A name is refused rather than tidied up, so My App is an error and not a silent my-app you did not choose.
An agent's first web app is normally called main, which is what every surface uses when you leave the name blank.
Creating a web app
Web apps are built through Co-build, the assistant in the Agent Builder action bar. There is no Web App tab and no editor: you describe the page you want, and the assistant writes it and publishes it onto the version you have selected.
Ask for the page in terms of what it is for and who uses it, in as much detail as you can give in one go:
- "Build an intake form for new referrals: name, contact details, the service they need, and free text for anything else. Save each submission and confirm on screen."
- "Add a page showing this month's open cases as a table, with a filter by owner and a total at the top."
Once one exists, the assistant can list what the version carries, read a page back before changing it, replace it, and remove it. Ask for any of those in the same conversation.
Three things are worth knowing before you start:
- Describe the whole page up front. Every change republishes the entire document, so a long conversation of small nudges costs considerably more than one good description followed by a few corrections.
- It publishes onto the version you have selected, so the usual version rules apply: work on a draft, check it in Preview, and deploy when it is right.
- A web app can have a working back end. As well as the page, the assistant can write server-side functions the page calls, with storage of their own. That is what lets a form actually save a submission and give the same answer every time, rather than asking the agent to do the saving and hoping it did.
Removing a web app takes it off that version only. A version that still carries it keeps serving it, exactly as knowledge files behave.
Preview
Preview in the Agent Builder action bar opens whichever previewer suits the selected version. A version that carries no web app gets the chat previewer. A version that carries one opens the page itself, in the same sandbox a visitor gets, so you are looking at the real thing rather than a mock-up.
If the version carries more than one, a picker at the top of the panel chooses between them.
Two notes appear under the frame when they apply:
- Preview only. The page is real but nothing behind it is, so anything that calls the agent will refuse. This is what a preview looks like when the session behind it could not be opened.
- This page comes from the version you are editing. The agent answering it is the deployed version, so new instructions in your draft are not in play yet. You are seeing the new page against the old behavior.
The 🔄 Restart preview button in the panel header starts the page over on a fresh conversation. It is the same button that reads New conversation on the chat previewer.
The preview panel opens wider than the chat panel, because a page laid out as a page does not survive a narrow column. Drag its left edge to resize it, and double-click the edge to go back to the default. Each kind of preview remembers its own width.
On a Salesforce page
Add Flourish Agent Platform: Web App in the Lightning App Builder or the Experience Builder. It works wherever the chatbot does: app pages, record pages, home pages, Experience Cloud pages, Flow screens, and the utility bar.
| Property | Description |
|---|---|
| Agent ID | The agent's Agent ID, from Copy Agent ID in the Agent Builder action bar. A Salesforce agent record ID also works. |
| Web App Name | Which of the agent's web apps to show. Lowercase letters, digits and single hyphens, such as intake-form. Leave it blank for main. |
| Conversation ID | Optional. Continue an existing conversation instead of starting a new one. |
| Group Key | Optional. Names which memory the agent uses, such as a record ID or an account number. See Choosing which memory to use. Agents set to No memory ignore it. |
| Record ID | Optional. On an Agent record page it selects the agent, so Agent ID can be left blank. |
| Custom Style (JSON) | Optional. Size and background, as JSON. See below. |
Record ID picks the agent; it does not pass page context. On any other record page it does nothing. The app knows what its own page and the agent's configuration give it, and nothing about the record it happens to be sitting beside.
Sizing it
The host picks the size, because the app cannot ask for one. A web app is served in a sandboxed frame, and neither Salesforce nor your website can measure how tall its contents are. Set the size in Custom Style (JSON). The app itself scrolls if it needs to.
{
"layout": { "height": "720px", "minHeight": "400px" },
"colors": { "background": "#ffffff" }
}
| Section | Keys | Default |
|---|---|---|
layout | width | 100% |
layout | height | 600px |
layout | minHeight | auto |
layout | maxHeight | none |
layout | borderRadius | 6px |
colors | background | white |
Any CSS length works, so 720px, 80vh and 100% are all fine. Clearing the property puts the defaults back.
This styles the box the component draws, not the app inside it. The page in the frame is served by the Agent Platform and is styled by the agent.
A web app can ask for a camera, a microphone, or the visitor's location from a Salesforce page. The component delegates those, along with clipboard access, fullscreen and autoplay. Delegation is not permission: the browser still prompts the visitor, who can still say no. An app that never asks for any of it is unaffected.
Each agent's app is served from an address of its own, which is also what lets an app keep anything in browser storage.
On your own website
The Agent Platform publishes a loader script that puts a web app on any page you can add HTML to. It is one script tag, and all of the logic lives in the hosted script, so improvements reach your site without you re-pasting anything.
What you need first
- A registered, deployed agent carrying a web app.
- A session endpoint on your own site. This is the one piece you build, and it is short. See below.
The snippet
<script src="https://cdn.toflourish.org/agent-platform/v1/webapp-embed.js"
data-agent-id="YOUR_AGENT_ID"
data-webapp="intake-form"
data-token-url="/api/flourish/session"
data-height="600" defer></script>
The app is drawn where the tag sits, so paste it where you want the app to appear.
The session endpoint
A web app opens its own connection to the agent, and it needs a short-lived session token to do it. Your API token must never reach the browser, so that token is minted on your server and the page is handed only the result.
data-token-url points at an endpoint on your own site. The loader fetches it on every page load and never caches the answer.
Your endpoint calls the Agent Platform:
curl -X POST https://agentplatform.toflourish.org/api/sessions \
-H "Content-Type: application/json" \
-d '{
"org": "YOUR_ORG",
"env": "YOUR_ENV",
"fl_api_token": "YOUR_TOKEN",
"agent_id": "YOUR_AGENT_ID"
}'
and returns what came back:
{
"conversation_id": "79a1df5e-13ff-4b81-aff3-a4f04a8324bf",
"token": "eyJhbGciOi...",
"expires_in": 3600
}
Pass the response through unchanged. The loader reads token and conversation_id, so an endpoint that proxies the call verbatim has nothing to map. See Authentication for where your organization identifier, token, and environment come from.
Two details worth getting right:
- Handle the
conversation_idquery parameter. The loader adds it to your endpoint's URL when the visitor already has a conversation going, andPOST /api/sessionstakes aconversation_idof its own. Pass it through and the visitor continues where they were. Ignore it and every page load starts a fresh conversation. - Do not cache the response. A session token lives about an hour. One cached and handed to the next visitor is a token that has already expired.
data-token renders a token straight into the page instead, with no endpoint. It is the simplest thing that works and the easiest to break: any page that is cached or built ahead of time ships a token that expired hours ago. The loader checks the expiry and refuses to render rather than showing an empty frame, and says why in the browser console. If both attributes are set, data-token-url wins.
Snippet attributes
| Attribute | Default | What it does |
|---|---|---|
data-agent-id | - | Required. Which agent. |
data-token-url | - | Your session endpoint. Recommended, and required unless you use data-token. |
data-token | - | A session token rendered into the page instead. |
data-webapp | main | Which web app. Same naming rules as above. |
data-height | 600 | A plain number is pixels. A CSS length passes through, so 100% and 100vh work. |
data-max-width | none | A width cap, in the same forms. |
data-title | App | The frame's accessible name, read by screen readers. Set it to something that describes the app. |
data-allow | none | Hardware the app may ask the visitor for. See below. |
data-test | - | Hides the app from everyone but you. See Test mode. |
With no data-agent-id, an invalid data-webapp, or no token source at all, the loader renders nothing rather than a broken shell, and names the reason in the browser console.
More than one web app can sit on one page. They are drawn in the order their tags appear, and two apps belonging to the same agent share a single session, so the agent sees both as one visit.
Hardware
An app that wants a microphone, a camera, or the visitor's location has to be allowed to ask:
data-allow="microphone; camera"
This grants nothing on its own. It makes the request possible; the browser still asks the visitor, who can still say no. Nothing is delegated by default, so an app that never asks for hardware costs nothing to leave as it is.
If a permission prompt never appears, this attribute is the first thing to check. Without it the request fails silently, with nothing on screen to explain why.
Test mode
Adding data-test hides the app from everyone. It renders only for someone who arrives with ?testflwebapp=1 in the URL, and it stays visible for them as they move around the site for the rest of that browser tab. ?testflwebapp=0 turns it back off without closing the tab.
This lets you paste the real snippet onto the real page and check it end to end before anyone else can see it. Remove the attribute to launch.
Driving it from the page
The loader puts window.FlourishWebApp on the page for the first app, and window.FlourishWebApps for all of them in the order they appear.
| Method | What it does |
|---|---|
session() | The conversation this app is on, when its token expires, and where the token came from. |
setHeight(value) | Resizes the frame. Takes the same forms as data-height. |
reload() | Mints a fresh token and starts the app over. |
terms() | Opens the Terms overlay the footer links to. |
debug() | What this app is doing, for a support ticket. The frame address comes back with the token removed. |
A page meant to stay open for hours should call reload() on a timer. A session token lives about an hour, and the app does not renew it on its own. This matters for a kiosk or a dashboard on a wall, and not for an ordinary page a visitor loads and leaves.
The loader also announces what it is doing as events on window, named the same way every Flourish embed names them:
window.addEventListener('flourish:webappsession', function (e) {
e.detail.conversationId; // the conversation this app is on
e.detail.expiresAt; // when its token expires
});
window.addEventListener('flourish:webappfailed', function (e) {
e.detail.reason; // why nothing was drawn
});
Anything the app itself announces is passed on to your page the same way, including events the loader has never heard of, so a new one reaches your listeners without you changing anything.
Messages only travel outward. Your page can watch what the app does; it cannot reach inside the app or drive it, which is what keeps a page the agent wrote isolated from the site hosting it. If you need a host page to drive an agent, that is what the Chatbot's Host API is for.
The footer
Every embedded web app carries the same footer the chatbot does: a note that this is AI and can make mistakes, a link to Flourish's Terms, and the Flourish credit line.
One conversation, not two
A web app and a chat window on the same page do not need separate conversations, and they are better off sharing one. A visitor who clicks Show me March totals in the app and then asks why is that so high? in the chat gets an answer, because the click was in the agent's context. Two conversations means the chat has no idea what happened in the app.
That is what Conversation ID on the Salesforce component is for, and what conversation_id on your session endpoint is for. Two apps from the same agent on one web page already share a session without you doing anything.
The conversation also survives a page change. When a visitor clicks a link on your site, the loader hands the same conversation to the next page, so a half-finished thread is still there. It ends when the browser tab closes, or after 30 idle minutes, so the next person on a shared computer never lands in someone else's.
Cost and limits
A web app spends money the same way every other agent surface does, and a page open to the public is the one that spends it without anyone watching.
- A conversation is capped at 200 requests every 2 minutes. That is a rate limit rather than a ban: it exists to catch a page that loops, and it drains on its own in minutes. It counts full agent turns, quick model calls and the app's own calls back to the agent alike.
- Your organization has a monthly spend limit covering every agent, chatbot, and scheduled call in it. An agent that reaches it is refused, and so is everything else in the org until the limit resets at the start of the next month, UTC. An agent can also have a ceiling of its own, set on the Cost tab, and by default it has none. Set one before putting a web app in front of the public, so a page nobody is watching cannot spend the organization's whole month.
- What the app itself calls is counted separately. An app's calls back to the agent have their own hourly budget, per agent rather than per conversation, and the monthly spend limits do not apply to them. So a form on a busy page keeps taking submissions after the org has stopped paying for chat. The flip side is that a genuinely popular page can exhaust that hourly budget and stay refused until the hour is out, so tell the visitor their work is not lost rather than asking them to try again in a moment.
Nothing reviews the page itself. Safety screening reads messages and replies, not markup, so the review is you looking at the page before you deploy the version that carries it. What is guaranteed is the isolation: a web app is served into a sandbox and cannot reach the page around it, your Salesforce session, or anything else on your site.