Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Embedding & Multi-tenancy

Use a scope to separate an agent's work by tenant, user, workspace, or record.

ValuePurpose
scope_idStable application identifier used to partition chat-session memory, stores, and credentials.
session_idOne conversation. Send it to continue that conversation.
contextApplication facts for the turn. The React widget calls this prop scopeContext.

Several conversations can share one scope. Reusing a scope does not resume a conversation.

Authenticate the scope

A scope partitions data, but it does not enforce access to existing sessions. The hosted history API accepts a caller-supplied scope filter, and chat can resume a session without comparing its scope with the token's scope. A scoped runtime token alone does not isolate tenants.

For a multi-tenant product, route runtime calls through your application backend:

  1. Authenticate each client request with your application's login.
  2. Authorize the requested scope and operation. For an existing session, check that the user can access that session in that scope. Apply these checks to chat, history, confirmations, and session actions.
  3. With the default platform JWT verifier, mint a short-lived runtime token on your backend using POST /api/agents/{agent_id}/tokens. Include the authorized scope and application facts:
{
  "ttl": 300,
  "scope_id": "tenant:acme",
  "context": {
    "tenant_id": "acme",
    "plan": "premium"
  }
}
  1. Keep the runtime token on your backend and use it to forward the authorized request. Record each created session's scope and ownership before allowing it to be resumed.
  2. Expose only the methods and paths your application needs. Set history filters on the backend and check session ownership before reads, changes, deletions, and actions. Reject unsupported paths.

Keep all runtime credentials on the backend, including short-lived tokens. Returning one to the browser lets the caller bypass your proxy's checks. Tokens minted by an automation token do not establish a separate product-user identity.

Direct browser calls to the runtime are appropriate only when callers are trusted to access the agent's sessions across scopes, such as an internal team app. Hosted team login grants agent access; it does not isolate customers within that agent.

Hosted JWKS federation has a separate verifier and membership requirements. A product JWT signed with your own shared secret does not automatically work on a hosted chat URL. See Authentication.

Choosing a scope ID

Application featureExample
Tenant support assistanttenant:acme
Case reviewcase:merchant-123
Portfolio assistantaccount:42

Use stable identifiers rather than display names. Renaming a customer should not change its data partition.

Claim and request precedence

A runtime-compatible verified JWT may carry these claims:

{
  "sub": "user-456",
  "scope_id": "tenant:acme",
  "context": { "tenant_id": "acme" }
}

Chat requests can also supply scope and context:

{
  "message": "Show open support tickets",
  "scope_id": "tenant:acme",
  "context": { "page": "support" }
}

A verified JWT scope_id overrides the request body's value. Without that claim, the runtime accepts the body's scope. Your backend must authorize and set the scope on every forwarded request. This precedence does not authorize access to a resumed session.

Context merges per key: claim values win on conflict, and other body keys remain. Those body values are untrusted. Do not use the merged scopeContext alone to authorize an operation.

React integration

Point the widget at your application proxy. The proxy serves the runtime paths used by the widget and preserves streaming responses. It authenticates the application token returned by getAppToken and adds its own runtime credential to authorized upstream calls.

import { ChatWidget } from '@amodalai/react/widget';
import '@amodalai/react/widget/style.css';
 
function AgentPanel({ tenantId, getAppToken }: { tenantId: string; getAppToken: () => Promise<string> }) {
  return (
    <ChatWidget
      key={tenantId}
      serverUrl="https://app.example.com/api/agent"
      scopeId={`tenant:${tenantId}`}
      scopeContext={{ tenant_id: tenantId }}
      getToken={getAppToken}
    />
  );
}

The application token authenticates the browser to your backend. The user widget prop does not send an identity. A key tied to the scope remounts the widget when the tenant changes; your backend still checks every request.

For an internal Amodal-hosted app whose users may access the agent's sessions across scopes, use same-origin calls and omit getToken. See Custom Runtime Apps.

Continue a conversation

A chat request without session_id starts a session. To keep one conversation across navigation, save its ID and pass it back when the panel remounts:

import { useState } from 'react';
import { ChatWidget } from '@amodalai/react/widget';
 
function ScopedChat({
  userId,
  scopeId,
  getAppToken,
}: {
  userId: string;
  scopeId: string;
  getAppToken: () => Promise<string>;
}) {
  const storageKey = `amodal-session:${userId}:${scopeId}`;
  const [resumeSessionId] = useState(() => localStorage.getItem(storageKey));
 
  return (
    <ChatWidget
      serverUrl="https://app.example.com/api/agent"
      scopeId={scopeId}
      getToken={getAppToken}
      resumeSessionId={resumeSessionId ?? undefined}
      onStateChange={({ sessionId }) => {
        if (sessionId) localStorage.setItem(storageKey, sessionId);
      }}
    />
  );
}

Mount ScopedChat with a key containing the signed-in user and scope. Its state initializer runs on mount. Clear saved conversation references on logout when a browser is shared. The storage key organizes client state. The backend must authorize the saved session before forwarding a resume or history request.

For a custom chat interface, read the session ID from the stream's init event. Send it as session_id to streamChat, or as sessionId in RuntimeClient.chatStream options. Load prior messages with getSessionHistory. Use the proxy URL and application authentication for these calls too.

Scoped resources

ResourceChat-session behavior
SessionsLabeled with a scope. History filters and resume do not enforce caller scope; the application backend must authorize access.
MemoryStored and recalled within the session's scope.
StoresAgent store tools use the session's scope. Stores marked shared use the global partition and are read-only through scoped tools.
Credentialsscope:KEY resolves a secret for the current scope.

Without a scope, chat uses the empty-string partition. This can be useful for a single-tenant agent, but does not separate tenants.

The direct /api/stores routes and React store hooks use the global partition. They do not inherit ChatWidget.scopeId or provide tenant-scoped access. Use scoped agent tools or an application backend that enforces the required partition for a multi-tenant data UI.

requireScope

In amodal.json:

{
  "scope": { "requireScope": true }
}

Chat routes using a static bundle reject an empty resolved scope. The hosted runtime loads bundles dynamically and does not apply this check. For hosted integrations, have your backend authorize the scope on every request and include it in the runtime tokens it retains. This setting does not authorize session access or protect the direct store API.

Shared stores

For reference data available to every chat scope, set shared in the store definition:

{
  "name": "product-catalog",
  "shared": true,
  "entity": {
    "name": "Product",
    "key": "{sku}",
    "schema": {
      "sku": { "type": "string" },
      "name": { "type": "string" }
    }
  }
}

See Stores for populating and using shared data.

Context injection

A connection can copy scope context into outbound HTTP requests. In its spec.json:

{
  "baseUrl": "https://api.example.com",
  "auth": { "type": "bearer", "token": "env:APP_API_TOKEN" },
  "contextInjection": {
    "tenant_id": {
      "in": "header",
      "field": "X-Tenant-Id",
      "required": true
    }
  }
}

The request includes X-Tenant-Id from scopeContext.tenant_id. A missing required value fails the request.

The generic request path supports header, query, path, and body injection. Native OpenAPI connections support header, query, and body injection; path injection fails preparation. Native query injection supplies defaults that model arguments can override. Enforce tenant authorization in the receiving API. See Connections.

Reading scope context in a tool

A custom tool can read the same context through ctx.scopeContext. Use it for application facts such as the current page or record. Use verified caller identity for permission checks.

Per-scope credentials

Reference a scope-specific secret in a connection:

{
  "auth": { "type": "bearer", "token": "scope:USER_API_TOKEN" }
}

The credential resolver looks up USER_API_TOKEN for the session's scope. In hosted deployments, manage scoped secrets through PUT /api/agents/{agent_id}/scopes/{scope_id}/secrets. Keep this operation on your backend.

A custom runtime host must supply a credential resolver for per-scope secrets. The runtime does not load .amodal/scopes.json automatically.

Inspect scope usage

Sessions provides scope filters and displays scope IDs in session details. Cost shows usage by scope. Amodal treats scope IDs as opaque strings; your application owns the mapping to customer or workspace names.