BuildersPut it in your productPut an agent on your page
Put an agent on your page
Runs a Mindset agent in your web app with one backend endpoint and the mindset-agent element, in plain HTML, React or Vue.
Skills for your coding agent
See it running: Trip planner, What’s wrong with this one?, Store assistant, Add a new employee, Ask your data, Guided intake, Widget round trip, Help widget, Halcyon
Add the billing-help Mindset agent to our billing app as a side panel. Our backend is Express and users are already signed in. Resume each user's last conversation.The SDK runs the agent in the browser. Its turn loop is a LangGraph state graph, the conversation's state lives in the page, and each model or tool call is one stateless request to Mindset, so replies stream with low latency and page tools run with no round trip. Your server makes one call: it creates a session with the org API key, which never reaches the browser. Sessions and trust explains what a session can reach.
Before you start#
- An agent you can embed, with an active version and Embedded in your own product ticked. See Build an agent.
- An org API key from Settings → API keys, in the environment you embed from.
- The agent's Embed tab in the Studio, which shows your Mindset host, org slug, environment slug and the agent's handle. The examples use the handle
billing-help.
1. Add a session endpoint#
A complete Express server for Node 18 or later, with "type": "module" in package.json. Any server stack works the same way.
import express from "express";
const MINDSET_HOST = "https://eu.mindset.ai"; // the host your Embed tab shows
const ORG_SLUG = process.env.MINDSET_ORG_SLUG;
const ENV_SLUG = process.env.MINDSET_ENV_SLUG;
const ORG_API_KEY = process.env.MINDSET_ORG_API_KEY;
const app = express();
// Your own sign-in middleware goes here. This example expects it to set req.user.
app.get("/api/session", async (req, res) => {
if (!req.user) return res.status(401).json({ error: "Not signed in" });
const r = await fetch(
`${MINDSET_HOST}/api/v1/orgs/${ORG_SLUG}/envs/${ENV_SLUG}/agent-sessions`,
{
method: "POST",
headers: {
"x-api-key": ORG_API_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
user: { email: req.user.email },
agent: "billing-help",
createUserIfNeeded: true,
}),
},
);
if (!r.ok) {
console.error("Mindset session create failed", r.status, await r.text());
return res.status(502).json({ error: "Could not start a session" });
}
// Forward the whole body, unchanged.
res.set("cache-control", "no-store").json(await r.json());
});
app.listen(3000);createUserIfNeeded: truecreates a Mindset user the first time one of yours shows up. Without it, every first-time user gets a 404.no-storestops a shared cache handing one user's session to another.- Forward the body whole. It holds the credential and the switched-on release features. Rebuilding it as
{ session }drops the features without an error.
Session API has every field, including identifying users by your own ID.
2. Put the agent on the page#
<script src="https://eu.mindset.ai/sdk/mindset-agent.js"></script>
<style>
mindset-agent { height: 600px; }
</style>
<mindset-agent agent="billing-help"></mindset-agent>
<script>
const agent = document.querySelector("mindset-agent");
agent.addEventListener("mindset:error", (e) => {
console.error("Mindset error:", e.detail.code, e.detail.message);
});
agent.configure({
getSession: async () => {
const r = await fetch("/api/session");
if (!r.ok) throw new Error("Could not start a session");
return r.json();
},
});
</script>The element draws nothing until configure(), which asks your backend for a session at once, so call it when the user can see the agent. The SDK calls getSession again whenever it needs a fresh session, so you write no refresh logic. The element fills its parent's height, at least 24rem, and renders in a shadow root, so your CSS and its CSS don't reach each other. Layout and theming covers sizing and colors.
3. Check it works#
Load the page, send a message and get a reply. If nothing appears, open the browser console: the element logs every problem there and raises it as mindset:error with a stable code. Errors and resilience maps each code and HTTP status to its fix.
React 19#
React 19 renders custom elements directly, so there is no wrapper package. Load the script once in index.html and use the tag in JSX. This component configures once, resumes a stored conversation, and reports the conversation ID and errors.
import { useEffect, useRef, type HTMLAttributes, type Ref } from "react";
type MindsetAgentElement = HTMLElement & {
configure(config: {
getSession: () => Promise<unknown>;
conversationId?: string;
conversationList?: boolean;
}): void;
};
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"mindset-agent": HTMLAttributes<HTMLElement> & {
agent?: string;
ref?: Ref<HTMLElement>;
};
}
}
}
async function getSession() {
const r = await fetch("/api/session");
if (!r.ok) throw new Error("Could not start a session");
return r.json();
}
type Props = {
conversationId?: string;
onConversation: (conversationId: string) => void;
};
export function BillingAgent({ conversationId, onConversation }: Props) {
const ref = useRef<HTMLElement>(null);
const configured = useRef(false);
const onConversationRef = useRef(onConversation);
onConversationRef.current = onConversation;
useEffect(() => {
const el = ref.current as MindsetAgentElement | null;
if (!el) return;
const handleConversation = (e: Event) => {
const detail = (e as CustomEvent<{ conversationId: string }>).detail;
onConversationRef.current(detail.conversationId);
};
const handleError = (e: Event) => {
const detail = (e as CustomEvent<{ code: string; message: string }>).detail;
console.error("Mindset error:", detail.code, detail.message);
};
el.addEventListener("mindset:conversation", handleConversation);
el.addEventListener("mindset:error", handleError);
// StrictMode runs this effect twice in development. Configure once.
if (!configured.current) {
configured.current = true;
el.configure({
getSession,
conversationList: true,
...(conversationId ? { conversationId } : {}),
});
}
return () => {
el.removeEventListener("mindset:conversation", handleConversation);
el.removeEventListener("mindset:error", handleError);
};
}, []);
return <mindset-agent ref={ref} agent="billing-help" />;
}- Guard
configure(). Under<StrictMode>, React runs each effect twice, and eachconfigure()creates a session. Theconfiguredref keeps it to one. - Pass
conversationIdtoconfigure(), never as a JSX attribute. The element writes its ownconversation-idattribute, and a re-render could write a different value back. - Keep
agentfixed. Changing it afterconfigure()raisesstale-configuration. To switch agents, render a new component with a changedkey.
Store the ID onConversation receives against the signed-in user, and pass it back as conversationId next visit. The mindset-agent element covers resuming and the conversation list.
Vue 3#
Tell Vue's compiler the tag is a custom element. With Vite, in vite.config.ts:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag === "mindset-agent",
},
},
}),
],
});Then configure the element once it's mounted:
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
const props = defineProps<{ conversationId?: string }>();
const emit = defineEmits<{ conversation: [conversationId: string] }>();
type MindsetAgentElement = HTMLElement & {
configure(config: {
getSession: () => Promise<unknown>;
conversationId?: string;
conversationList?: boolean;
}): void;
};
const agentEl = ref<MindsetAgentElement | null>(null);
function handleConversation(e: Event) {
const detail = (e as CustomEvent<{ conversationId: string }>).detail;
emit("conversation", detail.conversationId);
}
function handleError(e: Event) {
const detail = (e as CustomEvent<{ code: string; message: string }>).detail;
console.error("Mindset error:", detail.code, detail.message);
}
onMounted(() => {
const el = agentEl.value;
if (!el) return;
el.addEventListener("mindset:conversation", handleConversation);
el.addEventListener("mindset:error", handleError);
el.configure({
getSession: async () => {
const r = await fetch("/api/session");
if (!r.ok) throw new Error("Could not start a session");
return r.json();
},
conversationList: true,
...(props.conversationId ? { conversationId: props.conversationId } : {}),
});
});
onBeforeUnmount(() => {
agentEl.value?.removeEventListener("mindset:conversation", handleConversation);
agentEl.value?.removeEventListener("mindset:error", handleError);
});
</script>
<template>
<mindset-agent ref="agentEl" agent="billing-help" />
</template>The React rules apply here too. Without Vite, set the option in your build tool's Vue plugin.
Load the script on first open#
The drop-in script is about 1.5 MB. If most visitors never open the chat, load it on the first open:
const SDK_URL = "https://eu.mindset.ai/sdk/mindset-agent.js";
let sdkReady;
function loadMindsetSdk() {
sdkReady ??= new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = SDK_URL;
script.onload = () => resolve(customElements.whenDefined("mindset-agent"));
script.onerror = () => {
sdkReady = undefined;
reject(new Error("The Mindset SDK could not be loaded"));
};
document.head.appendChild(script);
});
return sdkReady;
}
async function openChat() {
await loadMindsetSdk();
const agent = document.createElement("mindset-agent");
agent.setAttribute("agent", "billing-help");
document.querySelector("#chat").appendChild(agent);
agent.configure({
getSession: async () => {
const r = await fetch("/api/session");
if (!r.ok) throw new Error("Could not start a session");
return r.json();
},
});
}
document.querySelector("#open-chat").addEventListener("click", openChat, { once: true });Load it from your Mindset host, since the element fetches charts and fonts from the same address. With a nonce-based content security policy, set script.nonce before appending it. In React or Vue, call loadMindsetSdk() before rendering the component that holds the tag.
Before you ship#
- Load the script once per page, and never add
mindset-agent-charts.jsyourself. A second copy logs a tag collision error. See SDK versions and updates. - Allow the Mindset host in your content security policy, if your site sends one.
- Every page load starts a fresh conversation unless you pass back a
conversationId.