Integrate & ship
Web embedding
Publish an agent, a swarm or a dashboard to your own site — where the visitors are anonymous, which changes what you must think about.
Open Integrations → Web Embedding. You create an embed key for one agent, swarm or dashboard, restrict it to the domains you control, and paste a snippet into your page.
Setting one up
- 1
Pick what to expose
One agent, one swarm, one published dashboard or one AI Analyst per key. Separate keys for separate placements — they can be revoked independently. - 2
Restrict the domains
List exactly the origins allowed to load it —www.example.com,docs.example.com. Never leave this open on a key with real data behind it. - 3
Set an expiry
Keys can carry an expiry date and be revoked or rotated later. A campaign key should outlive the campaign by days, not years. - 4
Copy the snippet
The dialog has two tabs: an iframe tag to paste where the widget should appear, or React SDK code if you would rather render it yourself.
<iframe
src="https://your-instance.example.com/embed/agent/emk_xxxxxxxxxxxxxxxx"
style="width:100%;height:600px;border:0"
title="Support assistant"
></iframe>Keys are prefixed emk_. The URL path segment matches the resource type: /embed/agent/<key>, /embed/swarm/<key>, /embed/bi/<key> or /embed/analyst/<key>.
Every field on an embed key
| Field | Default | Notes |
|---|---|---|
| Name | — | 1–80 characters. Your label; not shown to visitors. |
resource_type | — | One of agent, swarm, bi_dashboard. Fixed at creation. |
allowed_domains | empty | Origins permitted to load it. EMPTY MEANS NO DOMAIN RESTRICTION — set this. |
allow_ai | false | For dashboard embeds: whether viewers may use the Ask-AI follow-up. Off by default because each question is a model call billed to you. |
is_active | true | Turn off to disable the placement without deleting the key. |
transcript_retention_days | 30 | 1–3650. How long embed conversations are kept before the scheduled purge. |
expires_at | null | Optional expiry. |
use_count | 0 | Requests served — read-only, useful for spotting an abandoned placement. |
last_used_at | null | Read-only. |
Two ways to embed: iframe or React SDK
The snippet dialog offers both, on the same key. An iframe is a sealed box you drop on a page; the SDK is a library your React app calls, so the conversation renders with your own components. They reach the identical endpoints, so a key's domain allow-list, expiry, budget cap, guardrails and rate limits apply the same either way.
| iframe | React SDK | |
|---|---|---|
| Setup | Paste one tag. No build step. | npm install, then render a component or call a hook. |
| Look and feel | Our styling, inside a fixed frame. | Entirely yours — your bubbles, markdown renderer and theme. |
| Control | Sealed. The host page cannot read the conversation. | Full: send from anywhere in your app, intercept every streamed event, seed the history. |
| Layout | Fixed height, own scroll area. | A normal element in your layout and router. |
| Works for | Agents, swarms, dashboards and the AI Analyst. | Agents, swarms and the AI Analyst. Dashboards stay iframe-only. |
Dashboards are iframe-only, on purpose
Installing
The package lives in the repository at sdk/react and is not published to npm yet, so install it from the folder:
npm install ./sdk/reactAdd the host app's domain to the key's allowed domains before you start — the server checks the browser's Origin header on every call, exactly as it does for an iframe.
The drop-in component
<AgentChat> is the batteries-included path: a working chat with input, streaming replies and a stop button, themed through CSS variables so it inherits your palette without a stylesheet import.
import { AgentChat } from "@agentswarms/react";
export function SupportWidget() {
return (
<AgentChat
baseUrl="https://your-instance.example.com"
embedKey="emk_xxxxxxxxxxxxxxxx"
title="Support"
style={{ "--as-accent": "#7c3aed", height: 560 } as React.CSSProperties}
/>
);
}Themeable variables: --as-bg, --as-fg, --as-muted, --as-border, --as-accent, --as-accent-fg, --as-bubble and --as-radius.
The headless hooks
When the component's layout is not what you want, take the state and render it yourself. useAgentChat owns the streaming, cancellation and history; everything visual is yours.
import { useAgentChat } from "@agentswarms/react";
const { messages, send, stop, isStreaming, citations, widget, error } = useAgentChat({
baseUrl: "https://your-instance.example.com",
embedKey: "emk_xxxxxxxxxxxxxxxx",
// nodeId: "…", // swarm embeds: address one node
// initialMessages: [...], // seed a welcome message
// onEvent: (e) => {}, // every typed stream event, if you want the raw feed
});| Returned | What it holds |
|---|---|
| messages | The conversation. The last assistant message grows as the answer streams. |
| isStreaming | True while a reply is arriving. Use it to disable the input. |
| citations | Knowledge-base sources for the current answer, when the agent used any. |
| widget | A Visual BI chart spec, when the agent produced one. |
| error | Hard failures only — see the callout below. |
| send / stop / reset | Send a message, abort the stream, clear the conversation. |
Why it works this way
error — it arrives as an ordinary assistant message, because that is what the visitor should see, and it is what the iframe shows too. error is reserved for the cases where nothing was said at all: a revoked or expired key, an origin that is not allow-listed, a rate limit, or the network. Rendering refusals as errors is the usual way an SDK integration ends up looking broken when it is working correctly.The AI Analyst has its own hook, because it streams whole reasoning turns rather than text: activeTurn fills in live as the analyst states its approach and works through each step, and every finished turn is appended to turns. Follow-up questions carry the prior turns automatically.
import { useAgentAnalyst } from "@agentswarms/react";
const { turns, activeTurn, ask, isRunning, error } = useAgentAnalyst({
baseUrl: "https://your-instance.example.com",
embedKey: "emk_xxxxxxxxxxxxxxxx", // a key whose resource is an AI Analyst
});
// ask("What drove revenue last quarter?")Not a React app?
POST, and the parser is exported framework-free as createSseParser, mapChatFrame and mapAnalystFrame — enough to build the same integration in Vue, Svelte or no framework at all.The embed key is still public
Nothing changes about the trust model. The key ships in your JavaScript bundle exactly as it ships in iframe markup, and it is meant to: it is a site key, not a secret. Every control that matters runs on the server, so a reader who copies the key out of your bundle can only do what your allow-listed domain could already do — and disabling the key in Integrations → Web Embedding cuts off SDK apps as instantly as iframes.
What an anonymous visitor can reach
This is the part worth being precise about.
| Aspect | Behaviour |
|---|---|
| Identity | None. There is no sign-in; every visitor is anonymous. |
| Data access | The visitor has none of their own. The agent runs against the OWNER's knowledge and data, explicitly scoped to that owner. |
| Model cost | Billed to the key owner's workspace, under the owner's provider keys. |
| Tools | Only what the underlying agent has enabled — an embed does not add capability. |
| Guardrails | The agent's guardrails apply, including PII handling on input and output. |
| Which version runs | For an embedded SWARM, the published snapshot — not your working canvas. Creating the embed key publishes the current graph, and later edits stay private until you press Publish in the Deploy dialog. |
That last row is the one people are surprised by, and it is deliberate: an embed sits in someone else’s page, so a half-finished edit reaching it the moment you press Save would be the worst version of that behaviour. See API & webhooks for the publish states. Embeds created before publishing existed keep serving the live canvas until you publish once.
An embed is a public surface
How access is enforced
- Domain allow-list
- Requests carry the browser-set Origin header, which page scripts cannot forge, and are rejected when it isn't on your list. This stops your key being lifted and used on someone else's site — but it is a browser-level control, not authentication: a non-browser client can send any header it likes.
- Key lifecycle
- Keys record when they were last used and from which IP, can expire, and can be revoked or rotated. Rotation keeps the link between old and new so you can see what replaced what.
- Rate and concurrency limits
- Per-key limits blunt scraping and runaway loops.
- Budget caps
- A key can carry its own spend cap — see Budgets. On a public endpoint this is the difference between a bad day and a bad invoice.
Transcripts and retention
Embed conversations are recorded so you can see what people asked and how the agent answered. Each key has a transcript retention window (30 days by default, 1–3650); a scheduled purge deletes older transcripts.
Set this deliberately. Visitors may type personal information into a public chat box, and the shortest window that still serves you is the right one. Redaction guardrails can strip recognised personal data before it is stored or sent to a provider — see Guardrails & PII.
Embedded dashboards
A published dashboard can be embedded the same way. Widgets are sanitised on the way out so the underlying queries and connection details aren't exposed — but every number on the page is visible to whoever loads it.
Signed viewers — one dashboard, many customers
That last sentence is the problem when you are embedding analytics inside a product: every customer loading the page sees the same rows. Issuing one key each does not help — the keys are equally public, so any customer can use any other customer's.
Turn on Signed viewers (the shield button on a dashboard embed) and name the attributes your data is scoped by — tenant, region. You get a signing secret, shown once, and a ready-made Node snippet. Your backend mints a short-lived token naming the viewer and puts it in the iframe URL as ?vt=…. We verify the signature and turn those attributes into row filters. The browser can read the token; it cannot forge one.
- No token, an expired one, a forged one, or one missing a named attribute is a refusal that says which — never the owner's unfiltered view.
- An expiry is required and capped at 12 hours. A viewer token that never expires is a permanent grant sitting in someone's browser history.
- Attributes intersect:
tenantandregionmeans this tenant in this region. (IAM grants union — that is a different question.) - The embedded Ask-AI analyst reads the same scoped rows, not the owner's.
Widgets that can't be scoped are withheld, and say so
sum(revenue) by month — the total already contains every customer and no filter over those rows can recover one customer's share. Those widgets are withheld with that reason in place of the chart, because showing them unfiltered leaks and blanking them reads as "no data". Add the column to the widget's query to bring it back. Scoped viewers also see a banner naming the scope, so a subset is never mistaken for a total.Visual answers in embeds
If the agent has Visual BI answers enabled, embedded chats can return a chart alongside the text. Because the visitor has no data access, the chart is generated server-side using the owner's data with the owner enforced as the tenant boundary.
Embedding the AI Analyst
The fourth embed type puts the AI Analyst chat itself on your site. Visitors ask their own questions and see the stated approach, each step's result and chart, the findings and what to ask next — the same reasoning loop the signed-in screen runs.
It runs server-side as the analyst's owner, because an anonymous visitor has no datasets, no credentials and no query engine. That makes the analyst's data scope the access boundary: scoped to two datasets, it can read those two and nothing else. Scope it to what you would be comfortable publishing. Your IAM model rules and semantic row filters still apply, since the compile happens under your id.
This is the most exposed embed type
- Cost: several model calls per question, billed to you. Analyst turns are rate-limited to 5 per minute per key (a dashboard question gets 10), and spend is metered to the embed key so it shows up per-embed in Analytics.
- Latency, and what the visitor sees: a turn plans, queries, self-checks and synthesises — ~37–95s on the bundled HR sample. It streams, so the named stage and the stated approach land at about 6s and the trace fills in from there, rather than a spinner that is indistinguishable from a hang.
- Signed viewers do not apply. They filter stored results; an analyst writes new SQL each time, so a filter could cover the governed steps and not the rest — partial enforcement is a badge that vouches for less than it looks like.
What one visitor can consume
Worth knowing before you publish, because these are the numbers standing between a curious visitor — or a bot that finds the widget — and your provider bill. They are enforced server-side and counted in Postgres, so they hold across every app instance rather than per process.
| Limit | Value | Scope |
|---|---|---|
| Chat requests | 30 per minute → 429 | Per embed key |
| Dashboard “Ask AI” requests | 10 per minute → 429 | Per embed key |
| Resolve (widget load) | 60 per minute | Per embed key |
| Messages in one conversation | 60 | Per request |
| Conversation size | 200,000 characters | Per request |
| Spend | Your cap → 402, when ENFORCE_BUDGET_CAP is on | Per embed key, and per user |
Rate limits bound the pace, not the total
A swarm embed orchestrates in the visitor's browser
When an embed does not work
Every refusal below is deliberate and comes back as a message rather than a blank frame, so the fastest diagnosis is to read what the embed actually says. The browser console shows the same text with its status code.
| What you see | What it means | Fix |
|---|---|---|
| "This embed is not authorized to be called from this origin." | The page's origin is not on the key's domain allow-list. By far the most common one, and it usually appears the moment you move from local testing to a real site. | Add the exact origin — scheme and host, and www. counts. Staging and production are different origins. |
| "This embed key has expired." | The key had an expiry date and it has passed. | Create a new key and swap the snippet. Expiry cannot be extended after the fact. |
| "This embed has been disabled by its owner." | Someone toggled it off — often deliberately, during an incident. | Re-enable it in Integrations → Web Embedding, once you know why it was disabled. |
| "This embed key does not exist." | The key was deleted, or the snippet was copied with a character missing. | Compare against the key in the dialog. Deleted keys never come back — create a new one. |
| "This embed key is for a dashboard, not a chat." | The key's resource type does not match the endpoint or embed path being used. | Use the path for the type — /embed/bi/ for dashboards, /embed/agent/ for agents. |
| "Rate limited — please slow down." | One visitor, or one script, exceeded the per-key rate limit. | Usually nothing — it is the control working. Investigate if it persists, because it may be a script rather than a person. |
| A polite notice instead of an answer | The key's monthly budget is exhausted, or a guardrail refused the request. Both are answers, not errors, which is why nothing looks broken. | Raise or reset the cap in Budgets, or loosen the rule in Guardrails. |
Works on your machine, fails on the real site
Origin header on the API call, and the origin the embed page reports for the page hosting it. A reverse proxy, a preview deployment on a generated hostname, or an iframe nested inside another frame can each make the second one something you did not expect. Add every origin the widget genuinely loads from, and remember that https://example.com and https://www.example.com are two of them.Why a blocked embed still costs you nothing
Pre-publish checklist
- Domains restricted to sites you control.
- Only public-safe knowledge collections and tables attached to the agent.
- Tools limited to what a stranger may trigger.
- Guardrails on, with PII redaction if visitors might type personal details.
- A budget cap on the key.
- A transcript retention window you can justify.
- For a dashboard your customers each load: signed viewers on, with the scope attributes named — and check which widgets came back withheld before you ship.
- Tested by asking the agent, in the embed, to reveal its instructions and everything it knows.