Build/Agents

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 management
  • input - 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

Need Help?

Join our DiscordCommunity 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!