Skip to content

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. 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. 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. 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. 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.
html
<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

FieldDefaultNotes
Name1–80 characters. Your label; not shown to visitors.
resource_typeOne of agent, swarm, bi_dashboard. Fixed at creation.
allowed_domainsemptyOrigins permitted to load it. EMPTY MEANS NO DOMAIN RESTRICTION — set this.
allow_aifalseFor 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_activetrueTurn off to disable the placement without deleting the key.
transcript_retention_days301–3650. How long embed conversations are kept before the scheduled purge.
expires_atnullOptional expiry.
use_count0Requests served — read-only, useful for spotting an abandoned placement.
last_used_atnullRead-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.

iframeReact SDK
SetupPaste one tag. No build step.npm install, then render a component or call a hook.
Look and feelOur styling, inside a fixed frame.Entirely yours — your bubbles, markdown renderer and theme.
ControlSealed. The host page cannot read the conversation.Full: send from anywhere in your app, intercept every streamed event, seed the history.
LayoutFixed height, own scroll area.A normal element in your layout and router.
Works forAgents, swarms, dashboards and the AI Analyst.Agents, swarms and the AI Analyst. Dashboards stay iframe-only.

Dashboards are iframe-only, on purpose

A BI dashboard is a whole rendered surface — filters, drill-downs, cross-filtering and chart interactions — not a stream of messages. There is no useful way to hand that to a host app as data, so the SDK does not pretend to. The React SDK tab is disabled for dashboard keys.

Installing

The package lives in the repository at sdk/react and is not published to npm yet, so install it from the folder:

bash
npm install ./sdk/react

Add 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.

tsx
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.

tsx
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
});
ReturnedWhat it holds
messagesThe conversation. The last assistant message grows as the answer streams.
isStreamingTrue while a reply is arriving. Use it to disable the input.
citationsKnowledge-base sources for the current answer, when the agent used any.
widgetA Visual BI chart spec, when the agent produced one.
errorHard failures only — see the callout below.
send / stop / resetSend a message, abort the stream, clear the conversation.

Why it works this way

A guardrail refusal or an exhausted budget is not an 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.

tsx
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?

The wire format is plain Server-Sent Events over 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.

AspectBehaviour
IdentityNone. There is no sign-in; every visitor is anonymous.
Data accessThe visitor has none of their own. The agent runs against the OWNER's knowledge and data, explicitly scoped to that owner.
Model costBilled to the key owner's workspace, under the owner's provider keys.
ToolsOnly what the underlying agent has enabled — an embed does not add capability.
GuardrailsThe agent's guardrails apply, including PII handling on input and output.
Which version runsFor 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

Anything the agent can read, a visitor can ask it to reveal — including by writing a prompt that tries to talk it out of its instructions. Before publishing, ask: if a stranger asked this agent for everything it knows, what would come back? Attach only the collections and tables the public may see, and enable only the tools they may trigger.

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: tenant and region means 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

An embed renders stored results. If a widget projects your scope column, its rows can be filtered and the number is right. If it aggregated that column away — 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

A dashboard embed serves numbers you already computed and looked at. An analyst embed accepts a question and writes fresh SQL against whatever it is scoped to. Visitors never receive the generated SQL — it is stripped server-side, not merely left unrendered, because it names your tables and columns — and they get none of the owner tools (edit and re-run, pin to dashboard, verify, what-if). But the questions are theirs, so the scope is the control.
  • 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.

LimitValueScope
Chat requests30 per minute → 429Per embed key
Dashboard “Ask AI” requests10 per minute → 429Per embed key
Resolve (widget load)60 per minutePer embed key
Messages in one conversation60Per request
Conversation size200,000 charactersPer request
SpendYour cap → 402, when ENFORCE_BUDGET_CAP is onPer embed key, and per user

Rate limits bound the pace, not the total

Thirty chat requests a minute is roughly 43,000 a day if something hammers it continuously. The rate limit stops a burst; only a budget cap on the key stops the month. Set both — and set the cap before the embed is reachable, not after the first surprise.

A swarm embed orchestrates in the visitor's browser

The graph runs client-side, but each node's call carries only the node's id — the server re-reads that node's real provider, model, prompt and tools from your stored swarm. A visitor cannot select a more expensive model, change the prompt, or reach a node you did not publish. Swarms containing a human-approval step are refused for embedding outright, since no anonymous visitor can ever release the gate.

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 seeWhat it meansFix
"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 answerThe 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

Almost always the allow-list. There are two separate origin checks and they can disagree: the browser-set 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

Refusals happen before the model is called, so a wrong domain, an expired key or an exhausted budget cannot spend anything. Denials are audited with their reason, which is what makes "it stopped working on Tuesday" answerable — check the audit trail before assuming a code change broke it.

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.