Actions let the assistant do things in your app: navigate to a page, apply
a filter, prefill a form. For example, imagine that the user asks “show me Q3
for the EMEA region.” The assistant invokes your navigate and setFilters
actions, and your app responds — with the assistant narrating and reasoning
about the results.
Actions have two parts that work together closely:
To define action handlers, pass a handlers prop to your
<ClaritypeProvider>. Each ActionRegistry entry maps an action key to a
handler function along with a version number. When the assistant wants
to perform an action (subject to the user’s approval), the SDK will call the
appropriate function in your action registry. Here’s an example:
import { ClaritypeProvider, type ActionRegistry } from "@claritype/embed-react";
function App() {
const navigate = useNavigate();
const handlers: ActionRegistry = {
navigate: {
version: 1,
handler: async (params: { to: string }) => {
navigate(params.to);
return { landedOn: params.to, title: document.title };
},
},
setFilters: {
version: 2,
handler: async (params: { filters: Record<string, string> }) => {
applyFilters(params.filters);
return { resultCount: getResultCount() };
},
},
};
return (
<ClaritypeProvider orgId="acme" project="sales" origin="…" handlers={handlers}>
{/* … */}
</ClaritypeProvider>
);
}
Keys and versions must match the actions defined in your Claritype project — that pairing is the contract. An action defined in the project but missing a handler (or with the wrong version) is simply not available to the assistant in that session.
interface ActionHandler<Params = any, Result = any> {
version: number;
handler: (params: Params) => Promise<Result> | Result;
}
type ActionRegistry = Record<string, ActionHandler>;
Handlers can be synchronous or async. Parameters are validated against the
action’s JSON Schema on the Claritype side before an invocation ever reaches
you, so params will match the agreed shape.
Handler bodies are dispatched through a live reference: whatever closure you
passed on the latest render is what runs. Close over props, state, router
hooks — it all stays current, and you never need useCallback/useMemo
around the registry.
While handler bodies stay live, the set of keys and their versions is declared once, when the provider mounts. Adding, removing, or re-versioning a key later has no effect (the SDK warns in the console).
You should declare every action you might use up front. If some handlers are conditional on app state, check those conditions inside the handler:
// ✅ Always declared; behavior branches inside.
const handlers: ActionRegistry = {
exportReport: {
version: 1,
handler: async (params) => {
if (!featureFlags.exports) {
throw new Error("Exports are not enabled for this workspace.");
}
return doExport(params);
},
},
};
// ❌ The key set changes between renders — the late-added key is ignored.
const handlers = {
...(featureFlags.exports ? { exportReport } : {}),
};
The same applies to handlers that depend on lazily loaded code: declare the key at mount with a body that awaits the module, rather than adding the key once the module arrives. If the declared set genuinely must change, remount the provider (which starts a fresh session).
Whatever your handler returns becomes the action’s result — and its audience is the assistant, which uses it to reason about what happened and decide what to do next. Return what is now true, as structured data:
// Rich return tells the assistant where things stand.
return { landedOn: "dashboard", title: "Q3 Revenue", availableTabs: ["summary", "detail"] };
// Less helpful: technically successful but informationally empty.
return { ok: true };
However, you don’t need to repeat information that is available in passive context; the assistant will see those updates too before it writes its next response.
Results must be JSON-serializable. A sentence or two of description is fine in a pinch, but named fields are easier for the assistant to use reliably.
To report failure of an action, throw. The error’s message is delivered to the assistant as the action’s failure reason, so make it actionable the way you’d want a good API error to be:
handler: async (params: { orderId: string }) => {
const order = findOrder(params.orderId);
if (!order) {
throw new Error(`Order "${params.orderId}" not found — it may have been archived.`);
}
return { status: order.status, eta: order.eta };
};
A failed action isn’t a dead end: the assistant sees the message, can explain the situation to the user, and can try a different approach.
Claritype enforces server-side timeouts on invocations, scaled to the kind of action (navigation is expected to be quick; a form submission gets longer). If an action times out, the assistant is told so and handles it conservatively.
Every action in your project is configured with an intent classification — from read-only lookups to navigation to data-changing edits — and Claritype asks the user for confirmation before invoking sensitive actions, according to your project’s settings. By the time an invocation reaches your handler, any required consent has already been gathered.
Versions exist so action definitions can evolve without breaking deployed apps. The rules of thumb:
version declares which contract your code implements.
Claritype resolves each action to that version’s schema and description —
so your staging build can declare version: 2 while production still
declares version: 1.Turn on the provider’s debug prop during development. Action traffic —
every invocation and its result — is logged as it flows, and version
disagreements between a handler and the project are called out as console
warnings. It’s worth a quick console check after wiring up new actions: an
invocation for a key you didn’t register fails quietly from the user’s point
of view (the assistant is told no such handler exists and works around it),
but the debug log shows exactly what was invoked and what came back.