@amodalai/react
React bindings for a deployed Amodal runtime:
- Provider + hooks:
AmodalProvidergives every hook a configured client; hooks read stores, invoke tools and skills, follow sessions, and subscribe to live events. - Chat UI:
ChatWidget(and its presetAmodalChat) renders a full streaming chat with tool calls, confirmations, and widgets. Detailed on the Chat page. - Headless clients:
SdkClient,RuntimeClient, and the SSE/chat-api utilities, for when a hook doesn't fit (imperative code, non-React state layers).
Installation
npm install @amodalai/reactAmodalProvider
Wrap your app to configure the clients used by provider-based hooks:
import { AmodalProvider } from '@amodalai/react';
<AmodalProvider runtimeUrl={window.location.origin}>{children}</AmodalProvider>;| Prop | Required | Meaning |
|---|---|---|
runtimeUrl | yes | Base URL of the agent's runtime server. |
getToken | no | Returns a bearer token for authenticated requests (sync or async). |
metadata | no | Observability-only values sent with chat requests from provider-based hooks and AmodalAction; a map, or a function read on each send. See Request metadata. |
For an app served by Amodal at the agent's own origin, use a same-origin runtimeUrl such as window.location.origin and omit getToken: hosted login is cookie-backed and the bearer is injected at the edge. For direct bearer authentication by callers trusted across the agent's scopes, return a runtime-compatible token from getToken. Multi-tenant apps use an application proxy as runtimeUrl and return an application token instead. AmodalChat only inherits the provider's URL: pass its getToken separately. See Authentication for supported token types.
useAmodalContext() returns the provider's internals (client: RuntimeClient, sdk: SdkClient, runtimeUrl, getToken) for anything the hooks don't cover; it throws outside a provider.
Request metadata
Every chat request from the SDK carries metadata that tells Amodal where the message came from. It is recorded on the run's trace and the session and is never sent to the model. Amodal shows it on the trace, session, and feedback pages, and filters runs, sessions, and feedback by userId and screen.
The SDK collects these keys from the browser:
| Key | Value |
|---|---|
screen | Page path, without the query string or fragment. |
pageTitle | document.title, when set. |
referrer | Referrer origin and path, without query or fragment. |
userAgent | navigator.userAgent. |
locale | navigator.language. |
timezone | The browser's IANA time zone. |
viewport | Window size as <width>x<height>. |
Add what only your app knows through the metadata prop of AmodalProvider or ChatWidget. ChatWidget and AmodalChat send only their own metadata prop; the provider's value applies to provider-based hooks and AmodalAction. Your values replace the collected ones with the same key, so a readable screen name can replace the path. Pass a function to read current values on each message:
<AmodalProvider
runtimeUrl={window.location.origin}
metadata={() => ({ userId: currentUser.id, screen: currentScreenName(), plan: account.plan })}
>
{children}
</AmodalProvider>Values must be strings. A request accepts at most 50 keys, keys up to 64 characters, and values up to 1,024 characters. The runtime rejects a request over these limits, so the SDK drops any of your entries that breaks them, logs a console warning naming each one, and past 50 keys keeps yours over the collected ones. Any caller can send any value, so Amodal labels metadata unverified and shows the authenticated user beside it when the request carries one. Do not put secrets or sensitive personal data in metadata. The runtime reference describes the recorded attributes.
Reading and writing stores
These hooks call the runtime's direct store API. That API uses the global (empty-string) partition. The hooks do not inherit ChatWidget.scopeId; they read the same rows as agent store tools only when those tools use that partition. For tenant-scoped data, use scoped tools or an application backend that enforces scope. See Embedding.
useStoreQuery
const { data, error, isLoading, refetch } = useStoreQuery<Submission>('submissions', {
where: { status: 'received' },
orderBy: 'updated_at',
order: 'desc',
limit: 50,
});
// data: Array<{ key: string; value: Submission; updatedAt: string }>where is an equality filter on payload fields; orderBy/order sort server-side. Without limit, the runtime returns 20 rows; set limit to read more. Limits above 500 are fetched in 500-row pages. Call refetch() after an action to pull in new rows.
useStoreEntry
Read one document by key and update its payload:
const { data, isLoading, mutate, refetch } = useStoreEntry<Submission>('submissions', 'sub_bistro_ember');mutate(next) POSTs the payload and refetches. The runtime derives the document key from the payload's key template, so next must include its key field(s).
useStoreActions
Use get and set in event handlers:
const submissions = useStoreActions<Submission>('submissions');
await submissions.set('sub_x', row);set(key, value) accepts the key for compatibility; the runtime derives the stored key from value. The exported remove(key) calls a DELETE endpoint that the runtime store router does not implement.
Writes through mutate or set to a store declared shared return 403 unless the caller has an operator credential.
Running work
useToolRun
Invoke a durable tool ("execution": "durable" with an invoke trigger) from a button or form. run resolves with { sessionId, outcome }. Read the run status through useSession; tools can also write results to stores.
const analyze = useToolRun('analyze-submission');
await analyze.run({ submission_id });
// analyze.status: 'idle' | 'running' | 'succeeded' | 'failed'
// analyze.output.outcome, analyze.error, analyze.reset()run resolves when the session reaches a terminal or operator-facing state: if the runtime answers synchronously with an outcome it is used directly; a queued start is watched (polled) to its outcome. Pass scopeId and scopeContext in the second argument to useToolRun when the tool operates on a scoped resource.
useToolTrigger enqueues through POST /api/enqueue, then watches the session. It has the same launcher interface as useToolRun. See Tools.
useAutomation
useAutomation() schedules a durable tool, subagent, or prompt. schedule(target, { schedule, input?, label?, enabled? }) takes a tool name or { kind, ref } and a schedule of { cron, timezone? }, { every } (5m, 15m, 1h, 1d, or 1w), or { at }. It also returns list, enable, disable, and remove. useAutomations() lists the caller's automations; its intent filter is ignored.
useSkillAction
Send a chat prompt that asks the agent to use a declared skill, then collect its text response. The model chooses how to follow the prompt. Use a durable tool when the action requires a defined execution path.
const namer = useSkillAction('subject-line-writer');
namer.execute({ topic: 'renewal reminder', count: 3 }); // params are serialized into the generated prompt
// namer.result (string | null), namer.loading, namer.errorWorked example
This example displays global store rows and refetches them after a durable tool runs:
function SubmissionsScreen() {
const submissions = useStoreQuery<Submission>('submissions', { limit: 200 });
const findings = useStoreQuery<RiskFinding>('risk_findings', { limit: 200 });
const analyze = useToolRun('analyze-submission');
async function onAnalyze(id: string) {
await analyze.run({ submission_id: id });
await Promise.all([submissions.refetch(), findings.refetch()]);
}
if (!submissions.data?.length) return <EmptyState hint="send `seed` in chat first" />;
return <SubmissionsTable rows={submissions.data} findings={findings.data} onAnalyze={onAnalyze} />;
}Sessions
Use session hooks to display durable tool runs, scheduled work, and pending user input. The runtime history API hides sessions owned by another verified user and, when the token has a scope_id claim, sessions in other scopes. See Sessions. For a multi-tenant UI, point the provider at an application proxy that authorizes session access and retains runtime credentials.
| Hook | What it gives you |
|---|---|
useSessions(query?) | List the agent's sessions. The runtime ignores query. |
useSession(id) | Full detail for one session: status, metadata (surface, current step, pending input request, progress), messages. Poll via refetch, or pair with useAgentEvents for live updates. |
useSessionActions(id) | Operator actions: answer(value, edits?) resolves a pending requestInput (the runtime enforces eligible answerers), cancel(), restart(). |
useScheduledTools() | Sessions fired by schedules (cron), for a "scheduled runs" list. |
useToolCallLog({ sessionId }) | Per-session tool-call audit log (options also accept since and limit). |
Live events
useAgentEvents subscribes to the runtime's event bus over SSE (/api/events):
const { events, latest, isConnected } = useAgentEvents({
include: ["tool_calls"], // server-side filter: categories or event-type names
sessionId, // optional client-side narrowing to one session
onEvent: (e) => { ... },
});Type predicates (isToolCallStarted, isToolCallCompleted, isToolCallFailed) narrow the discriminated AgentEvent union. Reconnects resume from the last delivered event id.
The hook opens a plain EventSource and does not send getToken. On a hosted runtime it therefore relies on the same-origin session, receives events without content fields such as argsSummary and error, and receives every event type regardless of include. See Event Bus.
Chat components
AmodalChat and ChatWidget
ChatWidget supports positions, history, theming, callbacks, and custom transports. AmodalChat wraps it with position="inline" and defaults serverUrl from the nearest AmodalProvider.
import { AmodalChat } from '@amodalai/react';
<AmodalChat />;Props, positions, theming, and the widget's building blocks are documented on the Chat page.
AmodalAction
Fires a chat prompt from a button or other UI element:
import { AmodalAction } from '@amodalai/react';
<AmodalAction prompt="Summarize today's alerts" label="Get Summary" />;AmodalAction asks the model to follow a prompt. useToolRun invokes a named durable handler directly. The handler may itself call a model; determinism depends on its implementation.
Confirmation and review cards
Chat renders approval controls for confirmation_required events. These components are also exported for standalone approval UIs:
ConfirmCard: endpoint, method, reason, approve/deny.ReviewCard: the structured variant; full parameters, escalation badge, connection name.
Both take { confirmation, onApprove, onDeny }.
FormattedMarkdown, a prose-styled Markdown renderer, is exported for rendering agent output outside the chat surfaces.
Chat hooks
For a custom chat surface without ChatWidget:
| Hook | Description |
|---|---|
useAmodalChat | Provider-based chat session: send messages, streamed state, confirmation callbacks. |
useChat | The widget's chat state machine over /chat/stream, serverUrl-based. |
useChatStream | The transport-agnostic reducer underneath useChat; you supply streamFn. |
useSessionHistory | List, retag, rename, and delete previous chat sessions. |
useAmodalChat and useChat prepare a session through POST /sessions when they mount and send its ID with the first message, so the first reply skips the session setup. A hook that resumes a session prepares nothing, and reset prepares the next session. If the runtime cannot prepare one, the first message starts its own session. A prepared session that is still unused is rebuilt in the background when the page regains focus after most of the runtime's idle timeout has passed, before the runtime evicts it. useChat and ChatWidget also rebuild it after the token returned by getToken changes. An application proxy can therefore see POST /sessions requests beyond the one at mount. Pass prepareSession: false to useAmodalChat where a mount rarely leads to a message; AmodalAction does.
useAmodalBrief, useAmodalInsight, and useAmodalQuery each wrap one chat exchange through RuntimeClient.chatStream.
RuntimeClient.startTask and useAmodalTask call /task endpoints that neither the hosted runtime nor amodal dev serves. Use useToolTrigger with a durable tool for background work.
Headless clients
Everything the hooks do is available imperatively:
SdkClient(viauseAmodalContext().sdkor constructed directly):runTool,triggerTool,watchSession, session actions, store access via.runtime, files (readFile,listDir; hosted runtime only), automations.RuntimeClient:chatStream, store document reads.ChatClient/ChatStream: headless chat with typed events, no React.streamChat,createSession,listSessions,getSessionHistory(chat API),streamSSE/parseSSELine(SSE utilities): the lowest-level pieces, used by the clients above.
@amodalai/react/client exports RuntimeClient, ChatClient, ChatStream, and the chat API and SSE utilities without the components and hooks. SdkClient is exported from the package root only.
Unsupported endpoints
These call endpoints the runtime does not serve:
useAuth,useTeamMembers, anduseMemory. On a hosted runtime they return aNOT_IMPLEMENTEDSdkErrorthrough theirerrorfield. Underamodal devwith the bundled chat UI, the error isUNKNOWN.useConnections, which requests a connection list endpoint that does not exist.useStoreActions().remove, described above.SdkClient.writeFile, which callsPUT /api/files/*. Neither the hosted runtime noramodal devserves that route.