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 } from '@agentuity/runtime';
 
const agent = createAgent('Greeter', {
  handler: async (ctx, 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)

Route vs Agent Context

In agents, access services directly on ctx: ctx.logger, ctx.kv, ctx.thread, etc.

In routes, use Hono's context: c.var.logger or c.get('logger'), c.var.kv, c.var.thread, etc.

Adding LLM Capabilities

Most agents use an LLM for inference. Here's an agent using the AI SDK:

import { createAgent } from '@agentuity/runtime';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { s } from '@agentuity/schema';
 
const agent = createAgent('Assistant', {
  schema: {
    input: s.object({ prompt: s.string() }),
    output: s.object({ response: s.string() }),
  },
  handler: async (ctx, { prompt }) => {
    const { text } = await generateText({
      model: openai('gpt-5-mini'),
      prompt,
    });
 
    return { response: text };
  },
});
 
export default agent;

You can also use provider SDKs directly:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import OpenAI from 'openai';
 
const client = new OpenAI();
 
const agent = createAgent('Assistant', {
  description: 'An agent using OpenAI SDK directly',
  schema: {
    input: s.object({ prompt: s.string() }),
    output: s.string(),
  },
  handler: async (ctx, { prompt }) => {
    const completion = await client.chat.completions.create({
      model: 'gpt-5-mini',
      messages: [{ role: 'user', content: prompt }],
    });
 
    return completion.choices[0]?.message?.content ?? '';
  },
});
 
export default agent;

Or use Groq for fast inference with open-source models:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import Groq from 'groq-sdk';
 
const client = new Groq();
 
const agent = createAgent('Assistant', {
  description: 'An agent using Groq SDK with open-source models',
  schema: {
    input: s.object({ prompt: s.string() }),
    output: s.string(),
  },
  handler: async (ctx, { prompt }) => {
    const completion = await client.chat.completions.create({
      model: 'llama-3.3-70b-versatile',
      messages: [{ role: 'user', content: prompt }],
    });
 
    return completion.choices[0]?.message?.content ?? '';
  },
});
 
export default agent;

For streaming, structured output, and more patterns, see Using the AI SDK.

Adding Schema Validation

Define input and output schemas for type safety and runtime validation. Agentuity includes a lightweight built-in schema library:

import { createAgent } 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, 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 } 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, 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_...)
  ctx.current.name;        // This agent's name
  ctx.current.agentId;     // Stable ID for namespacing state keys
 
  // State management
  ctx.state.set('key', value);                   // Request-scoped (sync, cleared after response)
  await ctx.thread.state.set('key', value);      // Thread-scoped (async, 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('Support Ticket Analyzer', {
  description: 'Analyzes support tickets and extracts key information',
  schema: { ... },
  handler: async (ctx, input) => { ... },
});

The name is used for identification in logs, Workbench, and the Agentuity console. The description helps document what the agent does.

Adding Test Prompts

Export a welcome function to add test prompts for Workbench:

export const welcome = () => ({
  welcome: 'Welcome to the **Support Ticket Analyzer** agent.',
  prompts: [
    {
      data: JSON.stringify({ ticketId: 'TKT-1234', subject: 'Login issue' }),
      contentType: 'application/json',
    },
  ],
});
 
export default agent;

See Testing with Workbench for full setup and configuration.

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!