Tools and widgets

Tools let an agent act in your app, and widgets show your UI in its replies. Let’s connect both to the state your users already work with.

Tools

tools: {
  restart_service: {
    metadata: { title: "Restart a service" }, // transcript label; defaults to the tool's name
    description: "Restart one of the host app's services by name",
    parameters: { type: "object", properties: { service: { type: "string" } }, required: ["service"] },
    execute: async ({ service }) => await restartService(String(service)),
  },
}
  • The resolved value is returned to the agent as the tool result. A thrown error becomes a tool error.
  • Return JSON-compatible values. Omit absent object fields instead of setting them to undefined, and convert dates or custom objects to plain values.
  • The SDK names the tool when its result cannot be sent. Its action may already have happened, so read current state before retrying.
  • A string metadata.title labels the tool's transcript entry in prose instead of its registry name.
  • New tools reach the agent on its next run.

Widgets

widgets: {
  systemStatus: {
    description: "Shows the current status of the host app's systems",
    parameters: { type: "object", properties: { degraded: { type: "boolean" } } },
    render: ({ degraded }) => <StatusCard degraded={Boolean(degraded)} />,
  },
}
  • In React, render returns JSX. Elsewhere it draws into a container and may return a cleanup.
  • Renders live in your app's tree, so state, context, and event handlers keep working. An inline picker can call the same mutation function as a form elsewhere in your app.
  • Clicking a widget does not send a chat message or return a tool result. Have the agent read current state before its next change so it sees edits made through your UI.
  • Several renders of one widget can be live at once. The oldest collapse to a summary past a cap.
  • Dropping a widget disposes any render of it still in the transcript.

Schemas

parameters is a plain JSON Schema object, or any Standard Schema validator such as Zod, Valibot, or ArkType.

  • A Standard Schema is enforced in the browser before your execute or render runs.
  • With plain JSON Schema, nothing validates in the browser: treat the agent's input as untrusted.
  • Models sometimes send numbers as strings. With Zod, prefer z.coerce.number() over z.number().
  • The model is held to a schema only when every object in it lists its properties and allows no other keys. A free-form object such as z.record(), or a validator that cannot export JSON Schema, still works, but the model may then send input that does not match.

Typed definitions

defineTool and defineWidget are identity helpers that exist for their generics: with a Standard Schema in parameters, the execute or render input is the schema's own output type.

import { defineTool, defineWidget } from "@astralbeam/sdk/react"
import { z } from "zod"

const todoCard = defineWidget({
  description: "A single todo from the host app, addressed by its id",
  parameters: z.object({ id: z.coerce.number(), highlight: z.boolean().optional() }),
  render: ({ id, highlight }) => <TodoCard id={id} highlight={highlight ?? false} />,
})

const createTodo = defineTool({
  description: "Create a new todo and append it to the list",
  parameters: z.object({ text: z.string().min(1) }),
  execute: ({ text }) => addTodo(text), // text: string, validated before this runs
})
  • Import them from @astralbeam/sdk/react (JSX widgets) or @astralbeam/sdk/client (container widgets). @astralbeam/sdk/core has defineTool only, because its widgets carry no render.
  • With a plain JSON Schema, the input stays Record<string, unknown>, which is the honest type.

Live state

Definitions are declared once but called many turns later, so make sure they read current state.

  • In React, the SDK routes execute through the latest tools prop, so rebuilding the object each render is fine and keeps closures fresh.
  • Widget renders re-read the current widgets prop, so host state changes re-render projected UI.
  • Pass ids in widget props and resolve them against your own state, rather than snapshotting data into props.

Read after a write

An agent can call several tools before React renders again. Because state setters schedule a render, a second tool can read stale data if the first tool only called setState. Let’s keep the tool result and the next read in agreement.

  • For server data, await the mutation and read from the authoritative response or refreshed cache.
  • For browser data, commit to a synchronous store before returning. React can subscribe to that store with useSyncExternalStore.
  • Resolve issue, project, and assignee IDs inside the active Tenant. A valid schema does not establish ownership.
  • Keep manual edits and agent edits on the same mutation path so both validate, persist, and notify the UI in the same way.

Linearity’s store demonstrates this with localStorage. Its issue widgets receive only an ID and resolve the latest issue on each render.

Navigation is another host tool. Let’s give the agent URLs from your app’s own issue and project records, validate its destination, and pass the URL to your client router. Keep the sidebar in a persistent layout so opening an issue does not discard the conversation.

Linearity’s navigation tool accepts only known views and records in the active workspace. It rejects external URLs and another workspace’s IDs. Your production app must also enforce the current user’s permissions, just as it does for manual navigation.