---
title: "Embedding SDK"
description: "Embed an Agent-Native sidecar into an existing SaaS app with page context and host commands."
---

# Embedding SDK

## Installation {#installation}

```bash
pnpm add @agent-native/embedding
```

Subpath exports from `@agent-native/embedding`:

| Import path                        | What it provides                                                                        |
| ---------------------------------- | --------------------------------------------------------------------------------------- |
| `@agent-native/embedding`          | `EmbeddedApp` picker component, `getA2AUrl`, `getMcpUrl`, `sendMessage` (streaming A2A) |
| `@agent-native/embedding/react`    | React-specific hooks and components                                                     |
| `@agent-native/embedding/bridge`   | `announceEmbeddedAppReady`, `sendEmbeddedAppMessage` — used inside the embedded app     |
| `@agent-native/embedding/agent`    | Agent endpoint helpers                                                                  |
| `@agent-native/embedding/protocol` | Protocol types                                                                          |

For the **batteries-included embedded mode** (full sidecar with actions, database, and agent chat), install `@agent-native/core` on the server and use `createAgentNativeEmbeddedPlugin`:

```bash
pnpm add @agent-native/core
```

## Choosing a mode {#choosing-a-mode}

| Mode                                 | Use it when                                                                                         | Package                                                  |
| ------------------------------------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| **EmbeddedApp picker**               | Launching a full Agent-Native app as a focused iframe (asset picker, form builder, approval panel). | `@agent-native/embedding`                                |
| **Batteries-included server plugin** | Adding a durable agent sidecar with its own database and actions to your existing SaaS app.         | `@agent-native/core` + `createAgentNativeEmbeddedPlugin` |
| **`<AgentNative>` host component**   | Client-side: rendering the agent sidecar panel in your React app shell with live page context.      | `@agent-native/core/client`                              |
| **Extension slot**                   | Embedding a sandboxed mini-app (extension) inside an existing agent-native template.                | `@agent-native/core` extensions system                   |

The CLAW-style host bridge described below uses the batteries-included plugin (server) + the `<AgentNativeEmbedded>` component (client). It is the recommended default when you want the agent to see and operate on the page the user is already using.

---

The embedding SDK is for the CLAW-style shape: keep your existing SaaS app, add a durable agent sidecar, and let that agent see and operate on the page the user is already using.

Use it when you want an assistant that can:

- Read current page context: route, selected resource, highlighted text, active filters, user/org, and app-specific state.
- Call durable backend actions, MCP tools, or integration-backed tools from the sidecar app.
- Ask the host app to navigate, refresh data, remount a view, or open a resource after durable work completes.
- Run as an iframe/sidebar now, while leaving room for a no-iframe package or hosted template later.

## Embedded App And Picker Mode

Use `@agent-native/embedding` when the host product wants to launch a complete
Agent-Native app as a focused iframe surface: an asset picker, asset generator,
form builder, calendar slot picker, approval panel, or any other task-specific
workflow. This is intentionally smaller than the sidecar host bridge below: the
iframe announces readiness, the host can send named messages, and the embedded
app can emit domain events such as `chooseAsset` or `close`.

```tsx
import { EmbeddedApp } from "@agent-native/embedding";

export function AssetPickerDialog({ close }) {
  return (
    <EmbeddedApp
      url="https://assets.agent-native.com/picker"
      className="h-full w-full"
      onLoad={(ref) => {
        ref.postMessage("configure", {
          prompt: "Editorial blog hero",
          aspectRatio: "16:9",
        });
      }}
      onMessage={(name, payload) => {
        if (name === "chooseAsset") {
          const asset = payload as { url: string; altText?: string };
          insertAsset(asset.url, asset.altText);
          close();
        }
        if (name === "close") close();
      }}
    />
  );
}
```

Inside the embedded app, use the browser bridge to announce readiness and send
events back to the host:

```ts
import {
  announceEmbeddedAppReady,
  sendEmbeddedAppMessage,
} from "@agent-native/embedding/bridge";

announceEmbeddedAppReady({ app: "assets", mode: "picker" });
sendEmbeddedAppMessage("chooseAsset", {
  url: asset.previewUrl,
  assetId: asset.id,
  altText: asset.altText,
});
```

Assets also emits `chooseImage` as a compatibility alias for older image-picker
hosts; new integrations should listen for `chooseAsset`.

For hosted first-party apps, enable Cross-App SSO with Dispatch as the identity
hub so `content.agent-native.com` and `assets.agent-native.com` link users by
verified email. Iframe launches should still use short-lived, route-scoped
embed sessions when they need third-party-cookie resilience; normal app cookies
are not a complete embed auth story on their own.

The same package includes agent endpoint helpers for protocol discovery and
streaming text over A2A:

```ts
import { getA2AUrl, getMcpUrl, sendMessage } from "@agent-native/embedding";

getMcpUrl("https://assets.agent-native.com");
getA2AUrl("https://assets.agent-native.com");

for await (const chunk of sendMessage(
  "https://assets.agent-native.com",
  "Generate a blog hero",
)) {
  append(chunk);
}
```

## Batteries-Included Embedded Mode

For most SaaS hosts, use the full embedded runtime. The host mounts Agent-Native server routes into its existing app, passes its logged-in user to Agent-Native, and then renders the React sidebar/surface in the product UI. Agent-Native uses the host deployment, host session, and the configured `DATABASE_URL` to manage its own framework tables: chat threads, settings, application state, extensions, extension data, secrets, browser sessions, and action routes.

On the server:

```ts
// server/plugins/agent-native.ts
import { createAgentNativeEmbeddedPlugin } from "@agent-native/core/server";
import { builderActions } from "../agent-native/actions";
import { getBuilderSession } from "../auth";

export default createAgentNativeEmbeddedPlugin({
  databaseUrl: process.env.DATABASE_URL,
  auth: async (event) => {
    const session = await getBuilderSession(event);
    if (!session) return null;
    return {
      userId: session.user.id,
      email: session.user.email,
      name: session.user.name,
      orgId: session.organization.id,
      orgRole: session.organization.role,
    };
  },
  actions: builderActions,
  agentChat: {
    appId: "builder",
    systemPrompt:
      "You are Builder's embedded agent. Use Builder actions for durable work.",
  },
});
```

On the client:

```tsx
import {
  AgentNativeEmbedded,
  defineClientAction,
} from "@agent-native/core/client";

export function BuilderAppShell({ children, content, editor }) {
  return (
    <AgentNativeEmbedded
      defaultOpen
      session={{
        id: browserTabId(),
        label: "Builder editor",
      }}
      getContext={() => ({
        route: {
          name: "builder-editor",
          pathname: window.location.pathname,
          params: { contentId: content.id },
        },
        resource: {
          type: "content",
          id: content.id,
          name: content.name,
        },
        user: currentUser(),
        organization: currentOrganization(),
      })}
      actions={[
        defineClientAction({
          name: "select-element",
          description: "Select an element in the visual editor",
          schema: {
            type: "object",
            properties: { elementId: { type: "string" } },
            required: ["elementId"],
          },
          run: ({ elementId }) => editor.select(elementId),
        }),
      ]}
      onRefresh={() => queryClient.invalidateQueries()}
      onNavigate={(payload) =>
        router.navigate((payload as { path: string }).path)
      }
      onRemount={() => setAppKey((key) => key + 1)}
    >
      {children}
    </AgentNativeEmbedded>
  );
}
```

This mode is the recommended default because it reuses the full framework: backend actions are mounted under `/_agent-native/actions`, the agent can call the same actions as the UI, user-created extensions are stored in SQL, `extensionData` is durable and user/org scoped, and browser-session tools let the backend agent inspect or operate the currently open tab.

Host auth is server-side. Do not pass identity from the browser as the source of truth; use the host's request/session object or a short-lived server-verified token. If the host does not expose emails, return a stable `userId` and Agent-Native will use it as the owner key.

### Database Isolation

Embedded mode manages Agent-Native tables in SQL. For a mature SaaS product, the safest default is **same hosting and auth, dedicated Agent-Native database/schema**:

```ts
export default createAgentNativeEmbeddedPlugin({
  databaseUrl: process.env.AGENT_NATIVE_DATABASE_URL,
  auth: getHostSession,
  actions: hostActions,
});
```

Using the host product's main `DATABASE_URL` is supported, but make that an explicit choice. Agent-Native creates framework tables such as `settings`, `application_state`, `tools`, `tool_data`, browser-session tables, secrets, chat threads, and related indexes. A dedicated DB/schema avoids table-name collisions, keeps ownership of managed tables clear, and makes backup/retention policy easier to reason about. If you intentionally share the host DB, review existing table names first and treat Agent-Native tables as framework-owned.

## Host App

For standalone sidecar apps or cross-origin iframes, use the lower-level `<AgentNative />`. It renders the iframe sidecar and wires page context, live client actions, and host refresh/navigation commands in one place:

```tsx
import { AgentNative, defineClientAction } from "@agent-native/core/client";

export function AssistantDock({ customer, sessionToken }) {
  return (
    <AgentNative
      agentUrl="https://agent.example.com/workspaces/acme/sidecar"
      className="h-full w-full"
      session={{ id: browserTabId(), label: "Customer detail" }}
      auth={() => ({ token: sessionToken })}
      screen={{ includeVisibleText: true }}
      getContext={() => ({
        route: {
          name: "customer-detail",
          pathname: window.location.pathname,
          params: { customerId: customer.id },
        },
        resource: {
          type: "customer",
          id: customer.id,
          name: customer.name,
        },
        selection: {
          ids: getSelectedRowIds(),
          text: window.getSelection()?.toString() || undefined,
        },
        user: currentUser(),
        organization: currentOrganization(),
      })}
      actions={[
        defineClientAction<{ contentId: string }, { published: true }>({
          name: "publish-content",
          description: "Publish a Builder content entry",
          schema: {
            type: "object",
            properties: { contentId: { type: "string" } },
            required: ["contentId"],
          },
          destructive: true,
          approval: { title: "Publish this entry?", risk: "medium" },
          run: async ({ contentId }, { refresh }) => {
            await builderApi.publish(contentId);
            await refresh({ queryKey: ["content", contentId] });
            return { published: true };
          },
        }),
        defineClientAction<{ elementId: string }, void>({
          name: "select-element",
          description: "Select an element in the live visual editor",
          schema: {
            type: "object",
            properties: { elementId: { type: "string" } },
            required: ["elementId"],
          },
          run: ({ elementId }) => editor.select(elementId),
        }),
      ]}
      onNavigate={(payload) => {
        const { path } = payload as { path: string };
        router.navigate(path);
      }}
      onRefresh={(payload) => {
        const { queryKey } = payload as { queryKey?: readonly unknown[] };
        queryClient.invalidateQueries({ queryKey });
      }}
      onRemount={() => setAppKey((key) => key + 1)}
      onOpenResource={(payload) => openResource(payload)}
      onRequestApproval={(payload) => approvalDialog.confirm(payload)}
    />
  );
}
```

Use `screen={false}` if you only want explicit semantic context. Use `screen={{ includeDomHtml: true }}` as a fallback for apps that have not yet mapped their UI into semantic IDs and selection state. The host bridge only accepts messages from `agentUrl`'s origin by default. Pass `agentOrigin` if the iframe URL is a routed/proxied URL whose trusted origin differs.

For non-React hosts, call `createAgentNativeHostBridge()` directly and pass the same `getContext`, `actions`, and `commands` options.

## Iframe Side

Inside the Agent-Native sidecar, use the frame helpers to request host context, discover live browser-session actions, run them, or ask the host to do UI work. Always pass the expected `hostOrigin` in production:

```ts
import {
  announceAgentNativeFrameReady,
  createAgentNativeHostTools,
  requestAgentNativeHostActions,
  requestAgentNativeHostContext,
  runAgentNativeHostAction,
  sendAgentNativeHostCommand,
} from "@agent-native/core/client";

announceAgentNativeFrameReady({ hostOrigin: "https://app.example.com" });

const context = await requestAgentNativeHostContext({
  hostOrigin: "https://app.example.com",
});

const liveActions = await requestAgentNativeHostActions({
  hostOrigin: "https://app.example.com",
});

await runAgentNativeHostAction(
  "select-element",
  { elementId: context.selection?.ids?.[0] },
  { hostOrigin: "https://app.example.com" },
);

await sendAgentNativeHostCommand(
  "refreshData",
  { queryKey: ["customer", context.resource?.id] },
  { hostOrigin: "https://app.example.com" },
);

const hostTools = createAgentNativeHostTools({
  hostOrigin: "https://app.example.com",
});
```

## Server-Mediated Tool Bridge

For a CLAW-style coworker, the iframe can also register its live browser tab with the sidecar backend. The agent then gets normal backend tools that enqueue a request, the iframe claims it, the host page executes it, and the backend returns the result to the agent.

In the sidecar app, start the browser-session bridge once when the iframe mounts:

```tsx
import { useEffect } from "react";
import { startAgentNativeBrowserSessionBridge } from "@agent-native/core/client";

export function SidecarRuntime() {
  useEffect(() => {
    const bridge = startAgentNativeBrowserSessionBridge({
      hostOrigin: "https://app.example.com",
      label: "Builder editor",
    });
    return () => bridge.stop();
  }, []);

  return null;
}
```

The framework mounts `/_agent-native/browser-sessions` automatically. Once the bridge is running, the sidecar agent can use:

| Tool                           | Purpose                                                         |
| ------------------------------ | --------------------------------------------------------------- |
| `list-browser-sessions`        | See connected host tabs for the current user.                   |
| `view-browser-session`         | Ask a live tab for current page context and screen snapshot.    |
| `list-browser-session-actions` | Ask a live tab for current client-side action manifests.        |
| `run-browser-session-action`   | Run one current client action through the live tab.             |
| `send-browser-session-command` | Ask the host to refresh, navigate, remount, reload, or approve. |

This is the bridge to use when the agent is running on the backend, in Slack/Telegram/email, or as an A2A callee but still needs to touch the user's current browser tab when it is open. If the browser is closed, backend actions should still handle durable work and the browser-session tools will report that no active tab is connected.

## Actions

There are two action classes:

| Action kind    | Where it runs                                               | Works when browser is closed? | Best for                                                                                                 |
| -------------- | ----------------------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| Backend action | Sidecar app, backend API, MCP, or integration adapter       | Yes                           | Durable work like create, update, publish, sync, send, import.                                           |
| Client action  | Current browser tab through `<AgentNative actions={...} />` | No                            | Ephemeral UI work like select an element, read editor state, scroll to a row, copy current canvas state. |

Backend actions should be the default for anything that must survive refreshes, closed browsers, retries, or integration-triggered runs. They belong in the sidecar app's normal Agent-Native action/tool layer, where the agent can call them from chat, automations, Slack/Telegram/email integrations, and background jobs.

Client actions are a live bridge to one browser tab. The host advertises them with `source: "client"` and `availability: "browser-session"`, and the sidecar should treat that manifest as temporary. Re-list actions when route or selection changes, and fall back to backend actions when the tab disappears.

## Portable Extensions

The SDK also supports user-defined extensions: sandboxed Alpine.js mini-apps that a host SaaS can render in named slots. Use this when the customer wants to build their own small panels, calculators, dashboards, or workflow helpers against the same action/context surface that the agent uses.

```tsx
import {
  AgentNativeExtensionSlot,
  createHttpAgentNativeExtensionStorage,
  defineClientAction,
} from "@agent-native/core/client";

const storage = createHttpAgentNativeExtensionStorage({
  endpoint: "/api/agent-native/extensions/storage",
  headers: () => ({ Authorization: `Bearer ${sessionToken()}` }),
});

const actions = [
  defineClientAction({
    name: "list-at-risk-customers",
    description: "List customers currently at risk",
    schema: { type: "object", properties: {} },
    run: () => crmApi.customers.list({ status: "at-risk" }),
  }),
];

const customerHealthExtension = {
  id: "customer-health",
  name: "Customer health",
  description: "Shows at-risk customers and quick notes.",
  manifest: {
    slots: ["crm.customer.sidebar"],
    requestedActions: ["list-at-risk-customers"],
    requestedCommands: ["openResource", "refreshData"],
    storageScopes: ["user", "org"],
  },
  content: `
    <div x-data="{
      customers: [],
      note: '',
      async init() {
        this.customers = await appAction('list-at-risk-customers', {})
        const row = await extensionData.get('notes', slotContext.customerId, { scope: 'user' })
        this.note = row?.data?.text || ''
      },
      async save() {
        await extensionData.set('notes', slotContext.customerId, { text: this.note }, { scope: 'user' })
        await agentNative.refresh({ customerId: slotContext.customerId })
      }
    }" x-init="init()" class="space-y-3">
      <textarea class="w-full rounded-md border bg-background p-2" x-model="note"></textarea>
      <button class="rounded-md bg-primary px-3 py-2 text-primary-foreground" @click="save()">Save</button>
    </div>
  `,
};

export function CustomerSidebar({ customer, userExtensions }) {
  return (
    <AgentNativeExtensionSlot
      id="crm.customer.sidebar"
      extensions={[customerHealthExtension, ...userExtensions]}
      context={{ customerId: customer.id, plan: customer.plan }}
      actions={actions}
      storage={storage}
      storageContext={{
        userId: currentUser().id,
        organizationId: currentOrganization().id,
      }}
      getContext={() => ({
        resource: { type: "customer", id: customer.id, name: customer.name },
      })}
      commands={{
        refreshData: async () => queryClient.invalidateQueries(),
      }}
    />
  );
}
```

The manifest is the install contract. When `requestedActions`, `requestedCommands`, or `storageScopes` are present, the SDK enforces them in the host before an iframe request reaches the action bridge or storage adapter. When `slots` is present, `AgentNativeExtensionSlot` only renders the extension in matching slots. Hosts can still override policy per slot with `allowedActions`, `allowedCommands`, and `allowedStorageScopes`.

An extension is plain HTML. The iframe runtime provides the same safe bridge primitives to the mini-app:

```html
<div
  x-data="{ customers: [], async init() { this.customers = await appAction('list-at-risk-customers', {}) } }"
  x-init="init()"
>
  <template x-for="customer in customers" :key="customer.id">
    <button
      class="block w-full rounded-md px-3 py-2 text-left hover:bg-muted"
      x-text="customer.name"
      @click="agentNative.command('openResource', { type: 'customer', id: customer.id })"
    ></button>
  </template>
</div>
```

Available globals inside the iframe:

| Helper                         | Purpose                                                |
| ------------------------------ | ------------------------------------------------------ |
| `appAction(name, args)`        | Run a host-declared action.                            |
| `agentNative.context()`        | Read current host page, resource, slot, and user data. |
| `agentNative.command(name, p)` | Ask the host to navigate, refresh, remount, or open.   |
| `agentNative.refresh(payload)` | Shortcut for `refreshData`.                            |
| `extensionData.*`              | Persist extension-local data through the host adapter. |

By default, `extensionData` uses browser `localStorage`, which is useful for prototypes and local widgets. Production SaaS hosts should pass a backend-backed `storage` adapter so user and org scoped extension data is durable, auditable, and governed by the app's permissions. The generic HTTP adapter sends POST bodies like `{ operation, extensionId, slotId, collection, id, data, options, context }` and expects either `{ result }` or the result JSON directly.

This portable SDK layer is separate from the framework's built-in SQL-backed extension store. In an Agent-Native app, use the existing `ExtensionSlot`/`EmbeddedExtension` components and the `create-extension` action. In a hosted SaaS embedding scenario, prefer `createAgentNativeEmbeddedPlugin()` plus `AgentNativeEmbedded` when you want Agent-Native to manage extension definitions, approval, storage, and agent-created extensions out of the box. Use `AgentNativeExtensionSlot` only when the SaaS already owns extension definitions, approval, marketplace, storage, and billing.

Security model:

- Extension iframes are sandboxed without `allow-same-origin`; the mini-app cannot read the parent DOM, cookies, or app runtime directly.
- Extensions can only call the actions and commands allowed by the host and extension manifest.
- Risky actions should set `destructive` or `requiresApproval` so the host can show an approval flow.
- Treat user-authored extension HTML as untrusted. Review marketplace installs, log action usage, and scope backend storage by user/org.

## Sessions And Tabs

The host bridge is scoped to one iframe/host-window pair. If the same user opens multiple tabs, each tab has its own `session`, context, selection, client actions, and pending command responses. Do not assume a client action discovered in one tab can run in another tab, or that it will still exist after navigation.

For multi-tab products, keep durable state in SQL/backend actions and use client actions only for the tab-local parts: focusing a row, copying visible editor state, selecting a canvas element, or refreshing the current React Query cache. Include enough `route`, `resource`, and `selection` context for the sidecar to decide whether the current tab is the right place to run a browser-session action.

## Command Model

Built-in command names are deliberately app-shaped, not database-shaped:

| Command                                | Purpose                                                                |
| -------------------------------------- | ---------------------------------------------------------------------- |
| `navigate`                             | Move the host UI to a path/view/resource.                              |
| `refreshData` / `refresh-data`         | Ask the host to invalidate client-side data.                           |
| `remountView` / `remount-view`         | Ask the host to remount a subtree, e.g. `<App key={key} />`.           |
| `hardReload` / `hard-reload`           | Full browser reload.                                                   |
| `openResource` / `open-resource`       | Open a specific domain object in the host UI.                          |
| `requestApproval` / `request-approval` | Ask the host to show a confirmation flow. Register a handler for this. |

If no handler is provided, safe defaults dispatch browser events like `agentNative:refresh-data` and `agentNative:remount-view`. `requestApproval` has no default handler; register one before relying on it.

## Approval Guidance

Mark risky client actions with `destructive: true` in their manifest and require host approval before running operations that delete, publish, send, charge, invite, share, or otherwise affect users outside the current view. Backend actions should enforce their own authorization and approval checks too; host approval is useful UX, not the security boundary.

Prefer this shape:

- Durable mutation runs in a backend action with validation, auth, audit logging, and retries.
- Host command opens an approval UI or focuses the affected resource.
- Client action handles only the live UI step that cannot happen on the backend.

## Runtime Integration

Use `createAgentNativeHostTools()` inside the sidecar iframe when your agent runtime accepts plain tool descriptors. It returns four framework-agnostic tools:

| Tool                | Purpose                                                             |
| ------------------- | ------------------------------------------------------------------- |
| `view-host-screen`  | Read semantic host context and screen snapshot.                     |
| `list-host-actions` | List live browser-session actions exposed by the current tab.       |
| `run-host-action`   | Run one live client action by name.                                 |
| `send-host-command` | Send host commands such as refresh, navigate, remount, or approval. |

The helper intentionally returns plain `{ name, description, parameters, execute }` objects so sidecars can adapt them to the AI SDK, Anthropic, OpenAI function calling, or Agent-Native `ActionEntry` shape without coupling this SDK to one runtime.

## Recommended Product Shape

Start iframe-first. It works for Builder.io, customer SaaS apps, and internal admin tools without coupling release cycles or CSS/runtime assumptions.

The sidecar itself should still be an Agent-Native app/template: actions are the backend API surface, SQL-backed app state is the agent's memory, and integrations such as Slack or Telegram can route into the same durable chat. The embedding SDK supplies the live membrane between that sidecar and the current host page.
