Core concepts
Tools
Typed side effects agents can call, validated with Zod on the way in and out.
A tool is a typed side effect an agent can trigger: issue a refund, query a database, send an email. Every tool call passes through the tool broker, which validates the input with Zod, checks agent allowlists and policies, handles approvals, executes with retries and a timeout, and validates the output with Zod — so nothing enters or leaves a tool unvalidated.
Tools are defined once with tool({...}) and registered on a workflow via its tools option. Every call is recorded as a toolCalls entry on the run state and as events in the trace.
Defining a tool#
inputSchema validates arguments on the way in, outputSchema validates the result on the way out, and execute does the work.
import { tool } from "@assemble-dev/sdk";
import { z } from "zod";
export const issueRefund = tool({
name: "issueRefund",
description: "Issues a refund for an order",
inputSchema: z.object({ orderId: z.string().min(1), amount: z.number().positive() }),
outputSchema: z.object({ refundId: z.string(), status: z.literal("succeeded") }),
allowedAgentNames: ["refund-specialist"],
execute: async (input) =>
await Promise.resolve({
refundId: `re_${input.orderId}`,
status: "succeeded" as const,
}),
});tool({...}) validates its own configuration with Zod at definition time, so a missing name or a non-positive timeoutMs fails immediately, not mid-run.
Options#
| Option | Type | Description |
|---|---|---|
name | string | Unique identifier the workflow, policies, and bindings use to reference the tool. Required. |
description | string | What the tool does. Required. |
inputSchema | z.ZodType<TInput> | Zod schema every call's input is parsed against. Required. |
outputSchema | z.ZodType<TOutput> | Optional schema the result is parsed against. Without it, output passes through unvalidated. |
execute | (input: TInput) => Promise<TOutput> | The side effect itself. Receives already-validated input. Required. |
allowedAgentNames | string[] | Allowlist of agent names that may call this tool. Omit to allow any agent. |
requiresApproval | boolean | When true, every call pauses the run for a human decision. Defaults to false. |
sensitiveFieldNames | string[] | Input fields to redact in traces and uploads. Defaults to []. |
retry | { maxAttempts: number } | Number of execution attempts (minimum 1). Defaults to { maxAttempts: 1 }. |
timeoutMs | number | Per-attempt time limit. A timed-out attempt fails and counts against maxAttempts. No timeout by default. |
dryRun | boolean | Marks the tool as dry-run. Defaults to false. |
Validation, retries, and errors#
Each call moves through a fixed sequence:
- Input is parsed with
inputSchema. Invalid input fails the call with aVALIDATION_FAILEDerror beforeexecuteruns. - The agent allowlist is checked. A call from an agent outside
allowedAgentNamesis blocked. - Workflow policies run and may block, require approval, redact, or log the call.
executeruns up toretry.maxAttemptstimes. Each attempt is bounded bytimeoutMsif set; a timeout rejects that attempt withtool <name> timed out after <n>ms. If every attempt throws, the call fails with the last error.- The result is parsed with
outputSchema. A mismatch fails the call withVALIDATION_FAILED.
Inside a workflow, a failed call surfaces as an AppError — status 422 for validation failures, 500 for execution failures — and a blocked call surfaces as a 403. Unless the workflow is configured to continue on failure, the run fails with the tool error attached.
Warning: Retries re-run
executefrom scratch. Make tools idempotent — key side effects on an input field likeorderId— so a retry after a timeout cannot double-charge or double-send.
Binding tools to agents#
Register tools on the workflow with tools, then declare which agent triggers which tool with agentToolBindings. After the named agent's step completes, the workflow calls the tool with input you build from the run so far. buildToolInput receives { state, stepIndex, agentOutput }.
import { workflow } from "@assemble-dev/sdk";
import { z } from "zod";
export const supportWorkflow = workflow({
name: "support",
inputSchema: z.object({ ticket: z.string().min(1) }),
agents: [classifier, refundSpecialist],
tools: [issueRefund],
agentToolBindings: [
{
agentName: "refund-specialist",
toolName: "issueRefund",
buildToolInput: ({ agentOutput }) => ({
orderId: RefundDecisionSchema.parse(agentOutput).orderId,
amount: RefundDecisionSchema.parse(agentOutput).amount,
}),
},
],
});Bindings make the side effect deterministic and replayable: the tool call happens exactly once per step, with input derived from validated agent output. Referencing a tool name that is not registered in tools fails the run with a 404 listing the registered tools.
Agents can also declare allowedToolNames on their own definition — see agents for how the two allowlists relate.
Approvals and sensitive fields#
Set requiresApproval: true to force a human decision on every call — the run returns status: "awaiting_approval" and resumes once someone approves or rejects it in the approvals inbox or via resumeRun. For conditional gates ("only refunds over $100"), attach a policy with a requireApproval rule instead of hard-coding it on the tool.
sensitiveFieldNames marks input fields to redact before events are traced or uploaded, on top of the default redaction described in security. During replay, tool calls are mocked by default, so replaying a run never re-executes side effects.