Creating Agents
Build agents with createAgent(), schemas, and handlers
Each agent encapsulates a handler function, input/output validation, and metadata in a single unit that can be invoked from routes, other agents, or scheduled tasks.
Basic Agent
Create an agent with createAgent(), providing a name and handler function:
import { createAgent, type AgentContext } from '@agentuity/runtime';
const agent = createAgent('Greeter', {
handler: async (ctx: AgentContext, input) => {
ctx.logger.info('Processing request', { input });
return { message: 'Hello from agent!' };
},
});
export default agent;The handler receives two parameters:
ctx- The agent context with logging, storage, and state managementinput- The data passed to the agent (validated if schema is defined)
Adding Schema Validation
Define input and output schemas for type safety and runtime validation. Agentuity includes a lightweight built-in schema library:
import { createAgent, type AgentContext } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
const agent = createAgent('Contact Form', {
schema: {
input: s.object({
email: s.string(),
message: s.string(),
}),
output: s.object({
success: s.boolean(),
id: s.string(),
}),
},
handler: async (ctx: AgentContext, input) => {
// input is typed as { email: string, message: string }
ctx.logger.info('Received message', { from: input.email });
return {
success: true,
id: crypto.randomUUID(),
};
},
});
export default agent;You can also use Zod for more advanced validation:
import { createAgent, type AgentContext } from '@agentuity/runtime';
import { z } from 'zod';
const agent = createAgent('Contact Form', {
schema: {
input: z.object({
email: z.string().email(),
message: z.string().min(1),
}),
output: z.object({
success: z.boolean(),
id: z.string(),
}),
},
handler: async (ctx: AgentContext, input) => {
ctx.logger.info('Received message', { from: input.email });
return {
success: true,
id: crypto.randomUUID(),
};
},
});
export default agent;Validation behavior:
- Input is validated before the handler runs
- Output is validated before returning to the caller
- Invalid data throws an error with details about what failed
Schema Library Options
@agentuity/schema— Lightweight, built-in, zero dependencies- Zod — Popular, feature-rich, great ecosystem
- Valibot — Tiny bundle size, tree-shakeable
- ArkType — TypeScript-native syntax
All implement StandardSchema. See Schema Libraries for detailed examples.
Type Inference
TypeScript automatically infers types from your schemas:
const agent = createAgent('Search', {
schema: {
input: z.object({
query: z.string(),
filters: z.object({
category: z.enum(['tech', 'business', 'sports']),
limit: z.number().default(10),
}),
}),
output: z.object({
results: z.array(z.string()),
total: z.number(),
}),
},
handler: async (ctx, input) => {
// Full autocomplete for input.query, input.filters.category, etc.
const category = input.filters.category; // type: 'tech' | 'business' | 'sports'
return {
results: ['result1', 'result2'],
total: 2,
};
},
});Common Zod Patterns
z.object({
// Strings
name: z.string().min(1).max(100),
email: z.string().email(),
url: z.string().url().optional(),
// Numbers
age: z.number().min(0).max(120),
score: z.number().min(0).max(1),
// Enums and literals
status: z.enum(['active', 'pending', 'complete']),
type: z.literal('user'),
// Arrays and nested objects
tags: z.array(z.string()),
metadata: z.object({
createdAt: z.date(),
version: z.number(),
}).optional(),
// Defaults
limit: z.number().default(10),
})Schema Descriptions for AI
When using generateObject() from the AI SDK, add .describe() to help the LLM understand each field:
z.object({
title: z.string().describe('Event title, concise, without names'),
startTime: z.string().describe('Start time in HH:MM format (e.g., 14:00)'),
priority: z.enum(['low', 'medium', 'high']).describe('Urgency level'),
})Call .describe() at the end of the chain: schema methods like .min() return new instances that don't inherit metadata.
Handler Context
The handler context (ctx) provides access to Agentuity services:
handler: async (ctx, input) => {
// Logging (Remember: always use ctx.logger, not console.log)
ctx.logger.info('Processing', { data: input });
ctx.logger.error('Something failed', { error });
// Identifiers
ctx.sessionId; // Unique per request (sess_...)
ctx.thread.id; // Conversation context (thrd_...)
// State management
ctx.state.set('key', value); // Request-scoped (cleared after response)
ctx.thread.state.set('key', value); // Thread-scoped (up to 1 hour)
ctx.session.state.set('key', value); // Session-scoped
// Storage
await ctx.kv.set('bucket', 'key', data);
await ctx.vector.search('namespace', { query: 'text' });
// Background tasks
ctx.waitUntil(async () => {
await ctx.kv.set('analytics', 'event', { timestamp: Date.now() });
});
return { result };
}For detailed state management patterns, see Managing State.
Agent Name and Description
Every agent requires a name (first argument) and can include an optional description:
const agent = createAgent('Email Processor', {
description: 'Processes incoming emails and extracts key information',
schema: { ... },
handler: async (ctx, input) => { ... },
});The name is used for identification in logs, the Workbench, and the Agentuity console. The description helps document what the agent does.
Adding Test Prompts
The Workbench is Agentuity's development UI for testing agents locally and in production. Export a welcome function to customize the experience:
export const welcome = () => ({
welcome: `Welcome to the **Email Processor** agent.
This agent extracts key information from emails including:
- Sender and recipient
- Subject analysis
- Action items`,
prompts: [
{
data: JSON.stringify({ email: 'test@example.com', subject: 'Meeting tomorrow' }),
contentType: 'application/json',
},
{
data: 'Process this email',
contentType: 'text/plain',
},
],
});
export default agent;The prompts array provides quick-test options in the Workbench UI.
Best Practices
- Single responsibility: Each agent should have one clear purpose
- Always define schemas: Schemas provide type safety and serve as documentation
- Handle errors gracefully: Wrap external calls in try-catch blocks
- Keep handlers focused: Move complex logic to helper functions
import processor from '@agent/processor';
// Good: Clear, focused handler
handler: async (ctx, input) => {
try {
const enriched = await enrichData(input.data);
const result = await processor.run(enriched);
return { success: true, result };
} catch (error) {
ctx.logger.error('Processing failed', { error });
return { success: false, error: 'Processing failed' };
}
}Next Steps
- Using the AI SDK: Add LLM capabilities with generateText and streamText
- Managing State: Persist data across requests with thread and session state
- Calling Other Agents: Build multi-agent workflows
Need Help?
Join our Community for assistance or just to hang with other humans building agents.
Send us an email at hi@agentuity.com if you'd like to get in touch.
Please Follow us on
If you haven't already, please Signup for your free account now and start building your first agent!