Skip to main content

Web apps

How to build an agent's web app, try it, and put it on your own website.

Overview​

A web app is another way to reach an agent, alongside a chat window and the Daemon. Instead of a chat window, the agent gets a page, such as a form, a dashboard, a wizard, or a calculator. The page is designed for one agent, stored with it, and served by the Agent Platform already wired to that agent.

The difference from a chat window is what the visitor does. A chat window 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 another way in to the same agent.

You reach a web app in three places:

WhereWhat it's for
GreenhouseYou and your team, and checking the page while you work on the agent
Your own websiteAnyone, through a script tag
A Salesforce pageStaff and Experience Cloud visitors, through the Web App component. See Web apps on Salesforce pages.

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 doesn't change under them because you saved.

Names become part of a URL, so they're checked as one: 1 to 64 characters, lowercase letters, digits, and hyphens between them, such as intake-form or dashboard2. They can't have 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 didn't choose.

An agent's first web app is normally called main, which is what every way in uses when you leave the name blank.


Creating a web app​

Web apps are built by the Architect in Greenhouse. There's no web app section in the builder and no editor. You describe the page you want, and the Architect writes it and publishes it onto a version of the agent. In Salesforce, the Agent Builder's Co-build assistant does the same on the version you have selected.

Ask for the page by what it's for and who uses it, in as much detail as you can give at once:

  • "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 Architect 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 to know before you start:

  • Describe the whole page up front. Every change republishes the entire page, so a long conversation of small nudges costs much more than one good description followed by a few corrections.
  • It publishes onto a version, so the usual version rules apply: work on a draft, deploy it when it's right, and roll back if it isn't.
  • A web app can have a working back end. As well as the page, the Architect can write server-side functions, with storage of their own, that the page calls. That's what lets a form save a submission and give the same answer every time. Otherwise the agent would do the saving, and you'd have to hope it did.

Removing a web app takes it off that version only. A version that still carries it keeps serving it. Knowledge files work the same way.


Trying it​

In Greenhouse, Apps in the header of a chat with the agent lists the deployed version's web apps under From the builder. Choose one to open it beside the chat on the same conversation. That works in the test chat that Test opens in the builder and in any other chat with the agent. A link to the agent or a chat with &app=NAME on it opens that web app beside the chat (see Links).

What you see there is the deployed version's page, the one visitors get. In Salesforce, the Agent Builder's Preview shows the page from the version you're editing instead. See Preview.


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's one script tag, and all of the logic lives in the hosted script, so improvements reach your site without you pasting anything again.

What you need first​

  1. A registered, deployed agent carrying a web app.
  2. A session endpoint on your own site. This is the one piece you build, and it's 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"
}'

It then 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 passes the call's answer on as it is has nothing to map. See Authentication for where your organization identifier, token, and environment come from.

Two details to get right:

  • Handle the conversation_id query parameter. The loader adds it to your endpoint's URL when the visitor already has a conversation going, and POST /api/sessions takes a conversation_id of its own. Pass it through only for the visitor who started that conversation, and they continue where they were. See One conversation, not two. Ignore it, and every page load starts a fresh conversation.
  • Don't cache the response. A session token lives about an hour. A cached one handed to the next visitor may already have expired.

data-token renders a token straight into the page instead, with no endpoint. It's 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. It renders nothing rather than an empty frame and says why in the browser console. If both attributes are set, data-token-url wins.

Snippet attributes​

AttributeDefaultWhat 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-webappmainWhich web app. Same naming rules as above.
data-height600A plain number is pixels. A CSS length passes through, so 100% and 100vh work.
data-max-widthnoneA width cap, in the same forms.
data-titleAppThe frame's accessible name, read by screen readers. Set it to something that describes the app.
data-allownoneHardware 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. It names the reason in the browser console.

More than one web app can sit on one page. They're 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 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 try it 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.

MethodWhat 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 doesn't renew it on its own. This matters for a kiosk or a dashboard on a wall. It doesn't for an ordinary page a visitor loads and leaves.

The loader also announces what it's 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, but it can't reach inside the app or drive it. That keeps a page the agent wrote isolated from the site hosting it. To drive an agent from a host page, use the chat window's Host API.

Every embedded web app carries the same footer the chat window 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 don't need separate conversations, and they're 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. The click was in the agent's context. With two conversations, the chat has no idea what happened in the app.

That's what conversation_id on your session endpoint and Conversation ID on the Salesforce component are 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.

Your session endpoint decides who gets a conversation back. It mints with your organization's token, and the Agent Platform reopens any of your organization's conversations that a mint names. So an endpoint that passes the conversation_id parameter straight through lets anyone who has a conversation's ID continue it and read everything said in it. On a public site, pass an ID through only when your endpoint started that conversation for the same visitor. For example, keep the ID it minted in that visitor's own session on your site. Otherwise, start a fresh conversation.

A chat window that opens its own connection works differently. It starts each visitor as an anonymous visitor, and a conversation reopens only in the browser that had it. See Keeping the Conversation Across Pages.


Cost and limits​

A web app spends money the same way a chat does, and a page open to the public spends it without anyone watching.

  • A conversation is capped at 200 requests every 2 minutes. It's a rate limit 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, chat window, and scheduled call in it. An agent that reaches it is refused, and so is everything else in your organization until the limit resets at the start of the next month, UTC. An agent can also have a ceiling of its own, set in its Cost settings, and by default it has none. Set one before putting a web app in front of the public, so a page nobody is watching can't spend your 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 don't apply to them. So a form on a busy page keeps taking submissions after your organization has stopped paying for chat. A popular page can use up that hourly budget and stay refused until the hour is out. Tell the visitor their work isn't lost rather than asking them to try again in a moment.

Nothing reviews the page itself. Safety screening reads messages and replies, not the page, so look at the page yourself before you deploy the version that carries it. A web app is served into a sandbox, so it can't reach the page around it, your Salesforce session, or anything else on your site.