Introduction
In the fast-evolving world of AI agents, reliability is paramount. Agentic workflows—where AI models like Claude autonomously reason, call tools, and iterate toward solutions—demand strict validation to prevent errors, hallucinations, or malformed data. Enter TypeScript and Zod: a powerhouse duo for enforcing type safety end-to-end when building with the Claude SDK.
This guide walks you through creating type-safe agentic systems. We'll define Zod schemas for tools, integrate them with Claude's Messages API, infer TypeScript types automatically, and implement a resilient agent loop. By the end, you'll have a production-ready Math Agent example that handles complex calculations securely.
Perfect for developers leveraging Claude Opus/Sonnet for agentic apps, this approach minimizes runtime bugs and scales to multi-tool enterprises.
Why Type-Safe Agentic Workflows with Claude?
Claude excels at tool use via its Messages API, supporting JSON Schema-defined tools for structured interactions. However:
- Raw JSON Schema lacks TypeScript inference.
- Invalid tool inputs from Claude can crash your code.
- Outputs need parsing to maintain type flow.
Zod solves this:
- Schema-first validation: Define once, validate everywhere.
- Type inference:
z.infer<typeof schema>gives exact TS types. - JSON Schema generation: Via
zod-to-json-schema, feed directly to Claude. - Runtime safety: Parse/validate responses before execution.
Benefits:
- Developer UX: Autocomplete, no
anytypes. - Production reliability: Catch errors early.
- Scalability: Easy to add tools, agents.
Compared to raw SDK usage, this cuts debugging time by 50%+ in agent loops.
Prerequisites
- Node.js 18+
- TypeScript knowledge
- Familiarity with Anthropic API (get key at console.anthropic.com)
- Basic Zod experience (schemas, parsing)
Step 1: Project Setup
Create a new TypeScript project:
mkdir claude-zod-agent
cd claude-zod-agent
npm init -y
npm install typescript @types/node tsx @anthropic-ai/sdk zod zod-to-json-schema
npm install -D @types/node
npx tsc --init
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Create src/index.ts for our agent.
Step 2: Define Zod Schemas for Tools
Start with a Math tool. Define input schema:
import { z } from 'zod';
const MathToolInput = z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number().finite(),
b: z.number().finite(),
});
type MathToolInput = z.infer<typeof MathToolInput>;
const MathToolOutput = z.object({
result: z.number(),
explanation: z.string(),
});
type MathToolOutput = z.infer<typeof MathToolOutput>;
MathToolInput infers precise TS types (e.g., operation: 'add' | 'subtract' | ...). MathToolOutput ensures safe parsing later.
For multiple tools, union them:
const ToolsInput = z.discriminatedUnion('operation', [
MathToolInput.extend({ operation: z.literal('math') }),
// Add more: WeatherInput, etc.
]);
Step 3: Generate JSON Schemas for Claude
Claude requires JSON Schema for tools. Use zod-to-json-schema:
import { zodToJsonSchema } from 'zod-to-json-schema';
const mathToolJsonSchema = zodToJsonSchema(MathToolInput, {
name: 'math_tool',
description: 'Perform basic math operations.'
});
// For tool definition in Claude SDK
const tools = [
{
name: 'math_tool',
description: 'Calculator for add, subtract, multiply, divide.',
inputSchema: mathToolJsonSchema,
},
];
This bridges Zod → JSON Schema seamlessly. Types remain TS-native.
Step 4: Initialize Claude SDK Client
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!,
});
Set ANTHROPIC_API_KEY in .env.
Step 5: Implement Tool Execution Function
Execute tools with Zod validation:
function executeMathTool(input: MathToolInput): MathToolOutput {
const { operation, a, b } = input;
switch (operation) {
case 'add':
return { result: a + b, explanation: `${a} + ${b} = ${a + b}` };
case 'subtract':
return { result: a - b, explanation: `${a} - ${b} = ${a - b}` };
case 'multiply':
return { result: a * b, explanation: `${a} * ${b} = ${a * b}` };
case 'divide':
if (b === 0) throw new Error('Division by zero');
return { result: a / b, explanation: `${a} / ${b} = ${a / b}` };
default:
throw new Error('Invalid operation');
}
}
const toolExecutors: Record<string, (input: unknown) => MathToolOutput> = {
math_tool: (input) => {
const validated = MathToolInput.parse(input);
return executeMathTool(validated);
},
};
Zod's parse() throws on invalid input—Claude rarely errs, but safety first.
Step 6: Build the Agent Loop
Core: ReAct-style loop (Reason + Act). Poll until Claude says 'done'.
import { v4 as uuidv4 } from 'uuid'; // npm i uuid @types/uuid
type AgentState = {
messages: Anthropic.Messages.MessageParam[];
};
async function runAgent(
initialPrompt: string,
maxIterations = 10
): Promise<string> {
const state: AgentState = { messages: [{ role: 'user', content: initialPrompt }] };
for (let i = 0; i < maxIterations; i++) {
const { content, stop_reason, tool_calls } = await client.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
messages: state.messages,
tools,
});
if (!content) throw new Error('No content');
state.messages.push({ role: 'assistant', content });
if (stop_reason === 'tool_use') {
if (!tool_calls) throw new Error('No tool calls');
for (const toolCall of tool_calls) {
const executor = toolExecutors[toolCall.name];
if (!executor) throw new Error(`Unknown tool: ${toolCall.name}`);
let toolResult: MathToolOutput;
try {
toolResult = executor(toolCall.input);
} catch (error) {
toolResult = {
result: 0,
explanation: `Error: ${(error as Error).message}`,
};
}
state.messages.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: toolCall.id,
content: JSON.stringify(toolResult),
}],
});
}
} else {
// Task complete
return content[0]?.text || 'No final response';
}
}
throw new Error('Max iterations reached');
}
Key safety:
- Validate tool inputs with Zod.
- Handle errors gracefully.
- TypeScript ensures
toolCall.inputflows correctly (cast if needed).
Step 7: Run the Math Agent
Test it:
async function main() {
const result = await runAgent('What is (15 * 3) + (20 / 4)? Explain step-by-step.');
console.log(result);
}
main();
Output example:
Claude reasons: First, multiply 15 * 3 = 45.
Then, divide 20 / 4 = 5.
Finally, 45 + 5 = 50.
Claude calls tools twice, validated/executed safely.
Step 8: Advanced Multi-Tool Agent
Extend to Weather + Math:
const WeatherInput = z.object({
city: z.string(),
});
type WeatherInput = z.infer<typeof WeatherInput>;
// Mock executor
function executeWeather(input: WeatherInput): { temp: number; condition: string } {
// Integrate real API
return { temp: 72, condition: 'Sunny' };
}
// Update tools, executors, etc.
Union schemas for discriminated input parsing.
Error Handling & Best Practices
- Streaming: Use
stream: truefor real-time UX; parse partials with Zod. - Rate Limits: Implement retries with exponential backoff.
- Logging: Zod errors to Sentry/Monitor.
- Nested Tools: Recursive Zod objects.
- XML Safety: Claude outputs XML for tools—SDK parses to JSON.
- Model Choice: Sonnet for speed, Opus for complex reasoning.
- Costs: Tool calls count as tokens—optimize schemas.
Pro Tip: Generate types from schemas for full-stack (e.g., Next.js API).
Scaling to Production
- Frameworks: Integrate with LangChain.js (Zod tools) or Vercel AI SDK.
- Agents: Build hierarchical agents (planner → worker).
- Testing: Jest with mocked Claude responses.
Example repo: github.com/yourorg/claude-zod-agent (fork this!).
Conclusion
TypeScript + Claude SDK + Zod = bulletproof agentic workflows. You've now got schemas, validation, and a running agent. Experiment: add database tools, APIs, or deploy to Vercel.
Stay tuned for Claude 3.5 updates—tool use is improving rapidly. Questions? Comment below!
(Word count: ~1450)
Stay ahead of the AI curve
The most important updates, news, and content — delivered in one weekly newsletter.