Core concepts
Agents
A named model call with a role, instructions, and a typed output.
An agent is the smallest unit of work in Assemble: one named model call with a role, instructions, and an optional Zod output schema. You define agents once and compose them into teams and workflows — the workflow decides when each agent runs, and the runtime records every call in the trace.
Because an agent is just a frozen definition object, it carries no hidden state. The same agent can appear in several workflows, and everything it does at runtime — inputs, outputs, timings, errors — shows up as agentCalls on the run state and as events in the trace.
Defining an agent#
Call agent({...}) with a name, a role, a model adapter from @assemble-dev/providers, and instructions. The returned definition is immutable.
import { openai } from "@assemble-dev/providers";
import { agent } from "@assemble-dev/sdk";
import { z } from "zod";
const ClassificationSchema = z.object({
topic: z.enum(["refund", "technical", "qa"]),
urgency: z.enum(["low", "high"]),
});
export const classifier = agent({
name: "classifier",
role: "Classifies incoming support tickets",
model: openai({ model: "gpt-4o-mini" }),
instructions: "Classify the ticket topic and urgency.",
outputSchema: ClassificationSchema,
});The name is how everything else refers to the agent: supervisor route functions return it, tool allowlists check it, and trace events carry it. Keep names short, kebab-cased, and unique within any team or workflow that contains the agent.
The model is any adapter from @assemble-dev/providers — openai, anthropic, gemini, azureOpenai, bedrock, openrouter, cloudflareWorkersAi, or mockModel for keyless local runs and tests. See providers for adapter options.
Typed output with outputSchema#
Without outputSchema, an agent returns a string. With it, the runtime parses the model response against the schema and the agent returns a typed, validated object. Downstream agents, route functions, and your application code can rely on that shape.
const result = await supportWorkflow.run({ ticket: "refund please" });
if (result.status === "completed") {
console.log(result.output.reply);
}If the model output does not satisfy the schema, the runtime retries once with a schema-correction message appended to the conversation; if the retry also fails validation, the agent call fails with an AppError rather than silently passing malformed data down the pipeline. Prefer setting outputSchema on every agent whose output another agent or your code consumes. Schemas are matched structurally, so Zod v3 and v4 both work — see Zod versions.
Options#
All options accepted by agent({...}):
| Option | Type | Description |
|---|---|---|
name | string | Unique identifier used in routing, tool allowlists, and traces. Required. |
role | string | Short description of what the agent is responsible for. Required. |
model | ModelAdapter | Adapter from @assemble-dev/providers (openai, anthropic, gemini, azureOpenai, bedrock, openrouter, cloudflareWorkersAi, mockModel). Required. |
instructions | string | The system prompt for the agent. Required. |
inputSchema | z.ZodType | Optional schema the agent's input is validated against before the model is called. |
outputSchema | z.ZodType<TOutput> | Optional schema for the output. When set, the agent returns a validated TOutput instead of a string. |
allowedToolNames | string[] | Declares which workflow tools this agent intends to call. Stored on the definition for documentation and tooling; enforcement happens via allowedAgentNames on the tool. |
maxSteps | number | Step budget for the agent. With tools and maxSteps above 1, the agent runs a model-driven tool loop — see multi-step tool loops. Defaults to 1. |
repairToolCall | ({ toolCall, error, attempt }) => Promise<ModelToolCall | null> | One chance to fix a failed tool call — an unknown tool name or input that fails the tool's schema — before it becomes an error. See repairing failed tool calls. |
stopWhen | (step) => boolean | Per-step predicate over the tool loop; returning true ends the loop gracefully. See stopping the loop early. |
timeoutMs | number | Wall-clock limit for the model call. On expiry the agent fails with an AppError (Agent "<name>" timed out after <n>ms). |
costBudgetUsd | number | Spend budget for the agent, in US dollars. The run aborts with a budget error when estimated spend exceeds it — see cost budgets. |
fallbackModels | ModelAdapter[] | Ordered fallback chain of adapters — cross-provider is fine — tried when a model request fails with a retryable error after the primary's own retries. The run result's modelName reports whichever adapter actually served it. See cross-provider fallback. |
onAgentStart | (context) => void | Lifecycle hook, called before the model call. |
onAgentComplete | (context) => void | Lifecycle hook, called after a successful call. |
onAgentError | (context) => void | Lifecycle hook, called when the agent fails. |
Lifecycle hooks#
Hooks let you observe an agent without wrapping it. Each hook receives a small, typed context:
| Hook | Context |
|---|---|
onAgentStart | { agentName: string; input: JsonValue } |
onAgentComplete | { agentName: string; output: TOutput } |
onAgentError | { agentName: string; error: AppError } |
export const auditedClassifier = agent({
name: "classifier",
role: "Classifies incoming support tickets",
model: openai({ model: "gpt-4o-mini" }),
instructions: "Classify the ticket topic and urgency.",
outputSchema: ClassificationSchema,
timeoutMs: 30_000,
onAgentComplete: ({ agentName, output }) => {
console.log(`${agentName} finished`, output);
},
});Note: Hooks are for side observation — logging, metrics, alerting. Use policies to change behavior, and tracing for durable records; hooks do not appear in the trace.
When to give an agent tools#
Agents do not own tools. Tools are registered on the workflow, and access is enforced by allowedAgentNames on the tool — the tool broker blocks calls from agents outside a tool's allowlist. allowedToolNames on the agent is a declaration of intent for readers and tooling; it is not enforced at execution time.
Give an agent tools when its job includes a side effect — issuing a refund, sending an email, writing a record. Keep pure reasoning agents (classifiers, summarizers, reviewers) tool-free: they are cheaper to run, safer by construction, and trivial to test.
For deterministic tool execution after an agent step, use workflow-level agentToolBindings with buildToolInput — the workflow calls the tool with input built from the agent's output. See tools for how bindings, approvals, and retries interact.
Calling agents directly#
An agent does not need a workflow to run. .run(input, options?) executes it once and .stream(input, options?) streams text deltas as they arrive — including partial structured output when outputSchema is set. This is the path for embedding a single agent in an existing server, like a Next.js route handler or a Supabase Edge Function. See Library usage for run options, streaming patterns, multi-turn messages, and multimodal input.
Composing agents#
A single agent is rarely a product. Group related agents into a team for validated, reusable composition, then wire them into a workflow with a pipeline, supervisor, or parallel topology. The quickstart walks through the full path from one agent to a traced multi-agent run.