---
name: mindset-embed
description: Use when adding a Mindset (mindset.ai) AI agent chat to a web app with the drop-in mindset-agent element. Covers the backend session endpoint that keeps the org API key server side, letting visitors sign in on a page with no backend, loading the SDK script, configure(), conversation resume, sending from the page and knowing when the agent is busy, theming and CSS parts, React and Vue, moving from Mindset 3, and every error code.
metadata:
  title: Embed an agent with the element
  blurb: Puts an agent's chat in your web app with the mindset-agent element.
  group: In your app
  order: 1
  docs: put-an-agent-on-your-page, sessions-and-trust, the-mindset-agent-element, work-from-claude-code
  checked: d671041b6
  updated: 2026-10-21
---

# Embed a Mindset agent with the element

When you finish, people can talk to a Mindset agent in a chat panel on a page. The element draws the chat, so you write no chat UI. To draw your own UI instead, use the `mindset-uiless` skill.

There are two ways to give the element a session:

| The app | Do this | It needs |
|---|---|---|
| Has a backend that knows who is signed in | Steps 1 and 2: the backend creates a session and the page's `getSession` fetches it | An org API key on the server. The **Allowed websites** list plays no part |
| Has no backend | Step 2, then **No backend: visitors sign in on the page** | No API key. The Org lets people sign themselves up and allows the page's website |

Use the backend path whenever there is a backend. Your app then decides who the user is.

## Mindset in brief

Mindset is a hosted platform for building, running and governing AI agents. An **agent** is configuration, not code: a prompt, a model, and the resources it may use. Those resources are **scripts** (steps that must wait or happen in order), **functions** (fixed steps with one right answer), **widgets** and **connections** (the only way data enters or leaves). People manage all of it in the Studio, a web app, or through Mindset's MCP servers.

## Where you are building: host, org, environment

Every Mindset call names three things. Get all three from the person before you write code or call a tool. Never guess or invent them.

| Value | What it is | Where the person finds it |
|---|---|---|
| **Host** | The Mindset deployment their org lives on, for example `https://eu.mindset.ai` | The address bar when they're signed in to the Studio, or an agent's **Embed** tab |
| **Org slug** | Their company's account. The org is the isolation boundary: members, model keys, settings and audit sit here, and one org can never see into another | An agent's **Embed** tab, or the address under **Settings → Systems → Connect an AI client** |
| **Environment slug** | A named partition inside the org, such as Sandbox, Test or Production | An agent's **Embed** tab, or the same address. The **Environment** switcher in the Studio shows which one is current |

How environments behave:

- **Agents, scripts, functions, connections, triggers, schedules, conversations, runs and API keys each belong to exactly one environment** and can't see anything in another.
- **Every org starts with one environment, Sandbox, whose slug is `original`.** A slug never changes, even when the environment is renamed. Use the slug, never the display name.
- **Nothing copies between environments.** To promote a change from Test to Production, make the same change again in Production.
- **Don't create an environment to hold your work.** Environments can't be deleted, and they're for stages such as Test and Production, not for projects. Work in the environment the person names. Create one only if the person asks you to.
- **Members, model keys and org settings are shared by every environment.** Changing an org setting changes it everywhere.

## Credentials for code in an app: the org API key

Your app's backend authenticates to Mindset with an **org API key**.

- **An org admin creates it in the Studio under Settings → API keys.** It's shown once, when it's created. Ask the person to put it in their secret manager or environment. Never ask them to paste it to you.
- **A key works only in the environment it was created in.** Test code needs a Test key and production code needs a Production key.
- **It's admin-grade. It stays on the server.** Read it from an environment variable. Never put it in frontend code, a response body, a log line or a committed file.
- **Send it in the `x-api-key` header**, not in `Authorization`.

Keep one set of values per deployment stage, in the app's configuration:

```sh
MINDSET_HOST=https://eu.mindset.ai   # the person's host
MINDSET_ORG_SLUG=acme
MINDSET_ENV_SLUG=test                # the environment this deployment talks to
MINDSET_ORG_API_KEY=                 # secret: a key created in that same environment
```

A request with no `x-api-key` header gets `401`. A wrong org slug, a wrong environment slug, an agent that isn't in that environment, and a key without admin scope all return `404` `not_found`. The message says whether the org, the environment or the agent didn't resolve. An org or environment the key doesn't belong to reads exactly like one that doesn't exist, so nobody holding a key can probe for others. When you get a `404` during setup, check all four against the agent's **Embed** tab before anything else.

**Read the platform's own guide first.** If you hold the `mindset-admin` MCP server, call `get_information` with no arguments. When its index lists `embedding-readiness`, read that topic before you write any code: its checklist says whether this page or app can host an agent at all and what will snag, so tell the person what it finds. Then read `embedding` (sessions, setup, loading the script), `page-channels` (page tools, situational awareness, pass-through values) or `embedding-diagnosis` (error codes and fixes) for the work in hand. Those topics come from the deployment you are connected to, so they match its release: where they differ from this skill, follow them. When the index doesn't list them, the steps below apply as written.

## Ask the person for

- The host, org slug and environment slug for each deployment stage (dev, staging, production), and which environment each one talks to.
- The **agent handle**, from the agent's **Embed** tab, for example `billing-help`.
- Confirmation that the agent has an active version and **Embedded in your own product** ticked under its availability. New agents start with every availability box unticked. On the admin MCP server, `get_agent` shows that box as `availability.embed`, and `available: true` means the agent can be reached. Its `status` gates nothing unless it reads `archived`.
- With a backend: that an org API key exists for each environment and is in the app's secret store, under the name you'll read, and how the backend knows the signed-in user, so the session route can use their email or your own user ID.
- Without a backend: the page's website for each stage, exactly as the browser shows it (`https://app.example.com`, or `http://localhost:5173` while developing), and the Org admin's agreement to the settings under **No backend** below.

## Step 1: a session endpoint on the backend

The browser never holds the org API key. Your backend exchanges it for a short-lived session credential that runs one agent for one user, and the page fetches that from your backend.

Add one authenticated route to the app's backend, for example `GET /api/mindset-session`. It must know who the signed-in user is. It calls:

```
POST <host>/api/v1/orgs/<org-slug>/envs/<environment-slug>/agent-sessions
x-api-key: <org API key>
content-type: application/json
```

The body is strict JSON. Any field not listed here is a `400`.

| Field | Required | Notes |
|---|---|---|
| `user.email` or `user.externalId` | exactly one | Use `email` if the person might also use Mindset directly. Use `externalId` (letters, numbers, `_`, `-`, up to 100 characters) if they exist only in the app. Pick one form per user and keep it, or they become two users |
| `agent` | yes | The agent's handle or ID, from its **Embed** tab |
| `createUserIfNeeded` | in practice | Send `true`. Without it every first-time user gets a `404` |
| `attribution` | no | Your own string labels for the session: up to 16 keys |

An Express 5 route (Node 18 or later; any server stack works the same way). `requireSignedInUser` stands for the app's own sign-in middleware. On Express 4, wrap the body in `try`/`catch` and answer `502`, because Express 4 doesn't catch an `async` handler's throw:

```javascript
app.get("/api/mindset-session", requireSignedInUser, async (req, res) => {
  const r = await fetch(
    `${process.env.MINDSET_HOST}/api/v1/orgs/${process.env.MINDSET_ORG_SLUG}/envs/${process.env.MINDSET_ENV_SLUG}/agent-sessions`,
    {
      method: "POST",
      headers: {
        "x-api-key": process.env.MINDSET_ORG_API_KEY,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        user: { email: req.user.email },
        agent: "billing-help",
        createUserIfNeeded: true,
      }),
    },
  );
  res.set("cache-control", "no-store");
  res.status(r.status).type("application/json").send(await r.text());
});
```

Rules for this route:

- **Return Mindset's response body unchanged.** It holds `session` and `enabledFeatures`. Passing only `session` drops `enabledFeatures`, and the agent silently loses release features.
- **Send `cache-control: no-store`.** Never cache, log, decode or store the session. Keep it in memory in the page.
- **The Mindset route has no CORS headers.** Only a server can call it.
- **One session per agent.** Two agents on one page need two calls.
- **No renewal code.** When the SDK needs a new session it calls the page's `getSession` again, which calls this route again. An unused session expires after about 20 minutes.

Errors from the session call. Every error body is JSON with a `code` and a `message`.

| Status | `code` | Meaning |
|---|---|---|
| `400` | `validation_error` | A field is extra or malformed, both or neither of `email` and `externalId` were sent, or the agent has no published version to embed |
| `403` | `agent_not_available_here` | **Embedded in your own product** isn't ticked for the agent in the Studio |
| `404` | `not_found` | Wrong org slug, wrong environment slug, an agent not in that environment (or archived, or personal), a key without admin scope, or an unknown user without `createUserIfNeeded: true` |
| `409` | `no_version_in_force` | The agent has configuration versions but none is active |
| `409` | `user_not_active` | The user was invited but hasn't finished signing up, or was removed |

## Step 2: the element on the page

Load the SDK once per page, from the person's host, then add the element and configure it:

```html
<script src="https://eu.mindset.ai/sdk/mindset-agent.js"></script>

<mindset-agent id="chat" agent="billing-help"></mindset-agent>

<script>
  const chat = document.getElementById("chat");
  chat.configure({
    getSession: async () => {
      const r = await fetch("/api/mindset-session", { credentials: "include" });
      if (!r.ok) throw new Error("session " + r.status);
      return r.json();
    },
  });
</script>
```

- **The script registers `<mindset-agent>`.** Load it exactly once. A second copy logs a tag collision error. Never add `mindset-agent-charts.js`: the element loads it. Loaded with a plain `<script>` before your own code, the element is ready on the next line. Loaded `async` or `defer`, wait for `customElements.whenDefined("mindset-agent")`.
- **The element draws nothing until `configure()` runs.** With `getSession` it asks for a session straight away, so configure it when the user can see the chat. `configure()` returns nothing to wait for: `send()` and the setters work on the next line.
- **`getSession` is a function** that returns the parsed JSON body (`r.json()`), never a string, a `Response` or one field from the body. If it throws, the element asks again later.
- **`configure()` fields:** `getSession`, `autoProvision`, `org`, `env`, `agent`, `conversationId`, `conversationList`, `pageTools`, `situationalAwareness`, `extendedSituationalAwareness`, `passthroughParams`. Calling `configure()` again replaces the whole config.
- **Methods:** `configure`, `send`, `sendMessage`, `isAgentBusy`, `stop`, `setPageTools`, `setSituationalAwareness`, `setPassthroughParams`, `signOut`.
- **Attributes:** `agent`, `conversation-id`, `conversation-list`, `situational-awareness`, `passthrough-params`, `extended-situational-awareness`.
- **Events**, each with its data in `event.detail`: `mindset:runtime-event` (the runtime event), `mindset:error` (`{ code, message }`: branch on `code`), `mindset:conversation` (`{ conversationId }`, step 3), `mindset:busy` (`{ busy }`). Each runtime event also fires under its own name, such as `mindset:text-delta`, and Mindset 3's event names fire too. The element already logs every `mindset:error` to the console; listen when you want to show the person something.
- **Size:** the element fills its parent's height, with a 24rem minimum. Give the parent a height.
- **Theme** with `--ch-*` custom properties on the element. Most take three numbers (`124 58 237`). `--ch-panel`, `--ch-overlay` and the other channels the element reference lists take a full `rgba()` value.
- **Style one part** of the chat with `::part()`: `chat`, `composer`, `input`, `message-user` (the person's message bubble), `message-agent`, `page-event`, `quick-reply`, `tool-row`. A channel sets a color everywhere; a part reaches one surface. Set the text color wherever you set a background.
- **Don't use members that aren't listed here**, even if your editor offers them. They're internal and change without notice.

```css
mindset-agent { --ch-accent: 124 58 237; }
mindset-agent::part(message-user) { background: #ede9fe; color: #4c1d95; }
```

## No backend: visitors sign in on the page

The element shows a **Sign in with Google** button, signs the visitor in in a pop-up window, then chats as them. Configure it with the org and environment slugs instead of `getSession`:

```html
<script src="https://eu.mindset.ai/sdk/mindset-agent.js"></script>

<mindset-agent id="chat" agent="billing-help"></mindset-agent>

<script>
  document.getElementById("chat").configure({ org: "acme", env: "production", autoProvision: true });
</script>
```

It works only when all of these hold. Each is an Org admin's change, so confirm it with the person first:

- **The Org lets people sign themselves up.** The Studio has no switch for this. An Org admin turns it on through Mindset's admin MCP server: `org_domain_admin` with verb `set_self_signup` and `enabled: true`. It also lets anyone ask to join the Org from its sign-in page.
- **The agent is available to the Org's people:** **The Hub** and **Embedded in your own product** both ticked, and a version active. **The Hub** also lists the agent in the Org's Hub.
- **The page's website is allowed in that environment**, under **Settings → Environments → Allowed websites**, or with `org_domain_admin` verb `add_allowed_website` and an `origin`. Use the exact origin (`https://app.example.com`, `https://app.example.com:8443`). `https://*.example.com` covers subdomains. Plain `http` works only for `localhost`. Each environment has its own list, and an empty list signs nobody in.
- **The page is served over https** (or from `localhost`), and the browser allows the pop-up.

How it behaves:

- A Google account with a verified email signs in. Someone new joins the Org as a user, never an admin. A member signs in as themselves. A removed member is refused.
- The browser keeps the sign-in for this Org and environment, so a reload doesn't ask again. `chat.signOut()` ends it and shows the button again.
- `getSession` wins. With it in the config, `autoProvision` is ignored.
- A refusal arrives as `mindset:error` with code `invalid-configuration` when the visitor tries to sign in, and the pop-up says why. For a website that isn't allowed, the message names the exact origin to add.

## Step 3: keep the conversation across visits

Every page load starts a fresh conversation unless you hand one back.

1. Listen for `mindset:conversation` and store `e.detail.conversationId`. It fires after the first completed turn, and the element writes the same ID to its `conversation-id` attribute.
2. Store it per user: in your backend, or in browser storage under a key that names the user, which a static page has to ask your backend for. Another person's session can't open it, so one key per browser only loses history when people share a browser.
3. On the next visit, pass it as `conversationId` in `configure()`. An ID that can't be resumed (deleted, or another user's or environment's) starts a fresh conversation, announced on `mindset:conversation` with `replacedConversationId`. Always store the latest ID.
4. For a new conversation, `chat.removeAttribute("conversation-id")`, then call `configure()` without `conversationId`. Calling `configure()` again otherwise resumes the conversation in the `conversation-id` attribute.

## Send from the page, and know when the agent is busy

- **`send(text)` sends a turn as if the person typed it.** It returns a promise that settles when that turn ends. It never rejects: a failure raises `mindset:error` `turn-failed`.
- **Sends queue.** A send made while the agent is answering waits, and sends run in order. None is dropped, and none cuts an answer off.
- **`stop()` stops the running turn.** The partial reply stays, marked Stopped. Sends still waiting run next.
- **`isAgentBusy()` says whether a send made now would wait.** `mindset:busy` fires with `detail.busy` on each change, not at the start, so read `isAgentBusy()` once after `configure()`. Use it for your own progress indicator. You never need it before sending.
- **To report something that happened on the page, or give an instruction the person never sees**, use `send(text, { from: "page" })` or `sendMessage(text, { silent: true })`. The `mindset-page-tools` skill says when to use each.

```javascript
chat.addEventListener("mindset:busy", (e) => {
  document.body.classList.toggle("agent-working", e.detail.busy);
});
chat.send("What does my last invoice cover?");
```

## Coming from Mindset 3

The element answers to Mindset 3's names, so most page code carries over. Name the agent with the `agent` attribute and start it with `configure()`: there is no `agent-uid` attribute and no `window.mindset`.

| Mindset 3 | Here |
|---|---|
| `sendMessage(text)`, `sendMessage(text, { silent: true })` | Works. A message sent while the agent is busy is queued, not refused |
| `isAgentBusy()` | Works |
| `mindset:agent-busy`, `mindset:agent-idle`, `mindset:agent-registered`, `mindset:agent-error` | Fire. `mindset:agent-idle` fires first when the chat is ready |
| An event per runtime type, such as `mindset:text-delta` | Fires, as well as `mindset:runtime-event` |
| `situational-awareness` and `passthrough-params` attributes | Work, as JSON objects, applied at `configure()` and on every change. Anything else raises `invalid-attribute`. Removing one changes nothing: set `{}` to clear it |
| `extended-situational-awareness` | Works: 20,000 characters of situational awareness instead of 10,000 |
| Page tools as `{ name, description, parameters, handler }` | Accepted. See the `mindset-page-tools` skill |

## React and Vue

React 19 and Vue 3 render the element directly. There is no wrapper package and no npm package. Load the script once, in `index.html`.

- **React:** add a JSX declaration for `mindset-agent`, hold the element in a ref, and call `configure()` in `useEffect`. Guard it with a second ref so StrictMode doesn't configure twice and create two sessions.
- **Vue:** set `compilerOptions.isCustomElement` to accept `mindset-agent`, then call `configure()` in `onMounted` on a template ref.

## Rules

- Never put the org API key in frontend code, a response, a log or a committed file.
- Never put a secret in `passthroughParams`. Pass-through values are visible in the browser.
- Never turn on self-signup or add an allowed website without the person's say-so. Both change who can join the Org.
- On the no-backend path, any script on the page can read the stored sign-in. Use it only on pages whose scripts you trust.
- If the site sends a Content-Security-Policy, allow the Mindset host in `script-src`, `connect-src` and `font-src`.
- Switch on `event.type` in `mindset:runtime-event` and ignore types you don't know. Treat an unknown `mindset:error` code as a general failure. Both lists only grow.
- To let the agent act on the page (fill a form, navigate, read what's selected), use the `mindset-page-tools` skill.

## Check it works

1. With a backend, call the session route directly while signed in. Expect `200` with a JSON body containing `session`. Without one, open the page on an allowed website and sign in.
2. Load the page. The chat panel appears, and the browser console shows no `mindset:error`.
3. Send a message and get a reply.
4. Reload with the stored conversation ID and see the earlier messages.

## When it fails

Read the browser console first. The element logs every `mindset:error` there, with a stable code and a message naming the setting to change. A turn the person typed that fails shows its error in the chat instead.

| What you see | Cause and fix |
|---|---|
| The script failed to load | Wrong host in the script `src`, or the site's CSP blocks it. Use the host the **Embed** tab shows |
| The panel is very short or squashed | The parent has no height. Give the element or its parent a height |
| `mindset:error` `missing-agent` | No `agent` attribute. Set it to a handle from the org |
| `mindset:error` `invalid-auth` | `configure()` got neither `getSession` nor `autoProvision: true`, or `getSession` returned something unreadable or an expired session. Return `r.json()` from a fresh session call |
| `mindset:error` `turn-failed` | A turn the page sent with `send()` or `sendMessage()` failed. Check the backend logs for a failed session call, then the event's message |
| `mindset:error` `not-configured` | `send()`, `sendMessage()` or a `set` method ran before `configure()`, or after the element left the page |
| `mindset:error` `stale-configuration` | The `agent` or `conversation-id` attribute changed after `configure()`. Call `configure()` again |
| `mindset:error` `invalid-attribute` | `situational-awareness` or `passthrough-params` isn't a JSON object, or situational awareness is over its limit. Nothing changed |
| `mindset:error` `invalid-configuration` | Visitor sign-in can't work here: `org` or `env` missing, the page isn't https, self-signup is off, the agent isn't available to the Org's people, or the website isn't allowed (the message names the origin). Also raised for situational awareness over its limit in `configure()`, and for `signOut()` on an element using `getSession` |
| The pop-up says the deployment doesn't offer Google sign-in | That Mindset deployment has no Google sign-in. Use the backend path |
| The panel says the browser blocked the sign-in window | Allow pop-ups for the page |
| A status from the session call | See the error table in step 1 |

## Read more

Fetch these as Markdown when you need more than this skill gives you. The `get_information` topics come first, then this skill: if a page disagrees with either, follow them, not the page.

- https://docs.mindset.ai/docs/put-an-agent-on-your-page.md: read for complete React and UI-less examples.
- https://docs.mindset.ai/docs/sessions-and-trust.md: read for identity choice, attribution and session lifetime.
- https://docs.mindset.ai/docs/the-mindset-agent-element.md: read for every attribute, method, event and theming variable.
- https://docs.mindset.ai/docs/put-an-agent-on-your-page.md: read for React and Vue, including loading the script only when a user opens the chat.
- https://docs.mindset.ai/docs/layout-and-theming.md: read for a floating bubble, a side panel or dark mode.
- https://docs.mindset.ai/docs/content-security-policy.md: read if the site sends a CSP.
- https://docs.mindset.ai/docs/errors-and-resilience.md: read for every error and what the code should do.
