Learn

Examples

Practical examples of using the Agentuity JavaScript SDK

This section provides practical examples of using the Agentuity JavaScript SDK for common use cases. The examples are organized from beginner to advanced, progressively introducing features and patterns.

Getting Started

Hello World Agent

Here's a basic agent that processes requests and returns responses:

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('Hello', {
  handler: async (ctx, input) => {
    ctx.logger.info('Received request');
 
    return {
      message: 'Hello, World!',
      timestamp: new Date().toISOString()
    };
  }
});
 
export default agent;

Key Points:

  • Direct return values (no response object needed)
  • Input is automatically available
  • Logger accessed via ctx.logger
  • Returns plain JavaScript objects

Type-Safe Agent with Validation

This example demonstrates schema validation for runtime type safety and automatic validation:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
const agent = createAgent('UserRegistration', {
  schema: {
    input: z.object({
      email: z.string().email(),
      age: z.number().min(18)
    }),
    output: z.object({
      userId: z.string().uuid(),
      status: z.enum(['active', 'pending'])
    })
  },
  handler: async (ctx, input) => {
    // input.email and input.age are fully typed and validated
    return {
      userId: crypto.randomUUID(),
      status: 'active'
    };
  }
});
 
export default agent;

Key Points:

  • Schemas provide automatic validation and type inference
  • Input validation happens before handler execution
  • Output validation ensures consistent response shape
  • TypeScript provides full autocomplete

For advanced schema validation patterns including custom validation rules, transformations, error handling, and using alternative libraries like Valibot or ArkType, see the Schema Validation Guide.

Agent with Structured Logging

This example demonstrates structured logging for monitoring and debugging:

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('LoggingExample', {
  handler: async (ctx, input) => {
    // Different log levels
    ctx.logger.info('Processing request', {
      sessionId: ctx.sessionId,
      inputSize: JSON.stringify(input).length
    });
 
    ctx.logger.debug('Detailed processing info', { data: input });
 
    try {
      const result = await processData(input);
 
      ctx.logger.info('Processing successful', {
        resultSize: JSON.stringify(result).length
      });
 
      return result;
    } catch (error) {
      ctx.logger.error('Processing failed', {
        error: error.message,
        stack: error.stack
      });
      throw error;
    }
  }
});
 
async function processData(input: any) {
  // Simulate processing
  return {
    processed: true,
    data: input,
    timestamp: Date.now()
  };
}
 
export default agent;

Key Points:

  • Use structured logging with metadata objects
  • Available log levels: trace, debug, info, warn, error, fatal
  • Logs are automatically indexed and searchable
  • Include context like sessionId for request tracing

Core Features

Key-Value Storage

This example demonstrates how to use the key-value storage API for data persistence:

import { createAgent } from '@agentuity/runtime';
import * as v from 'valibot';
 
const agent = createAgent('PreferenceManager', {
  schema: {
    input: v.object({
      action: v.union([
        v.literal('get'),
        v.literal('set'),
        v.literal('delete')
      ]),
      userId: v.string(),
      preferences: v.optional(v.any())
    }),
    output: v.object({
      message: v.optional(v.string()),
      preferences: v.optional(v.any()),
      success: v.boolean()
    })
  },
  handler: async (ctx, input) => {
    const { action, userId, preferences } = input;
 
    if (action === 'get') {
      const result = await ctx.kv.get('user-preferences', userId);
 
      if (!result.exists) {
        return { success: false, message: 'No preferences found' };
      }
 
      const userPrefs = await result.data.json();
      return { success: true, preferences: userPrefs };
    }
 
    if (action === 'set') {
      await ctx.kv.set(
        'user-preferences',
        userId,
        preferences,
        { ttl: 60 * 60 * 24 * 30 } // 30 days in seconds
      );
 
      return { success: true, message: 'Preferences saved' };
    }
 
    // Delete
    await ctx.kv.delete('user-preferences', userId);
    return { success: true, message: 'Preferences deleted' };
  }
});
 
export default agent;

Key Points:

  • ctx.kv.get() returns a result with exists flag
  • ctx.kv.set() supports optional TTL for expiring data
  • Data can be stored as JSON objects or strings
  • Storage is namespaced by the first parameter

Vector Search & Semantic Retrieval

This example demonstrates how to use vector storage for semantic search:

import { createAgent } from '@agentuity/runtime';
import { type } from 'arktype';
 
const ProductType = type({
  id: 'string',
  name: 'string',
  description: 'string',
  price: 'number',
  category: 'string'
});
 
const agent = createAgent('ProductCatalog', {
  schema: {
    input: type({
      action: '"index"|"search"|"delete"',
      query: 'string?',
      products: type({ array: ProductType }).optional()
    }),
    output: type({
      message: 'string?',
      ids: 'string[]?',
      results: 'unknown[]?',
      count: 'number?',
      deletedCount: 'number?'
    })
  },
  handler: async (ctx, input) => {
    const { action, query, products } = input;
 
    if (action === 'index') {
      if (!products || products.length === 0) {
        throw new Error('No products to index');
      }
 
      const documents = products.map(product => ({
        key: product.id,
        document: product.description,
        metadata: {
          id: product.id,
          name: product.name,
          price: product.price,
          category: product.category
        }
      }));
 
      const ids = await ctx.vector.upsert('products', ...documents);
 
      return {
        message: `Indexed ${ids.length} products`,
        ids
      };
    }
 
    if (action === 'search') {
      if (!query) {
        throw new Error('Query is required');
      }
 
      const results = await ctx.vector.search('products', {
        query,
        limit: 5,
        similarity: 0.7
      });
 
      const formatted = results.map(result => ({
        id: result.id,
        key: result.key,
        ...result.metadata,
        similarity: result.similarity
      }));
 
      return {
        results: formatted,
        count: formatted.length
      };
    }
 
    // Delete
    if (!products || products.length === 0) {
      throw new Error('No product IDs to delete');
    }
 
    const productIds = products.map(p => p.id);
    const deletedCount = await ctx.vector.delete('products', ...productIds);
 
    return {
      message: `Deleted ${deletedCount} product(s)`,
      deletedCount
    };
  }
});
 
export default agent;

Key Points:

  • ArkType provides concise TypeScript-first syntax
  • Vector upsert stores documents with metadata
  • Search supports similarity threshold and result limits
  • Metadata filtering available via search params

Object Storage with Public URLs

Using object storage for files and generating public access URLs:

import { createAgent } from '@agentuity/runtime';
import { s3 } from 'bun';
import * as v from 'valibot';
 
const agent = createAgent('FileManager', {
  schema: {
    input: v.object({
      action: v.union([
        v.literal('upload'),
        v.literal('download'),
        v.literal('share'),
        v.literal('delete')
      ]),
      filename: v.string(),
      data: v.optional(v.any()),
      expiresIn: v.optional(v.number())
    }),
    output: v.object({
      success: v.boolean(),
      url: v.optional(v.string()),
      data: v.optional(v.any()),
      message: v.optional(v.string())
    })
  },
  handler: async (ctx, input) => {
    const { action, filename, data, expiresIn } = input;
 
    if (action === 'upload') {
      const file = s3.file(`documents/${filename}`);
      await file.write(data);
 
      return {
        success: true,
        message: 'File uploaded successfully'
      };
    }
 
    if (action === 'download') {
      const file = s3.file(`documents/${filename}`);
      const exists = await file.exists();
 
      if (!exists) {
        return {
          success: false,
          message: 'File not found'
        };
      }
 
      return {
        success: true,
        data: await file.text()
      };
    }
 
    if (action === 'share') {
      const url = s3.presign(`documents/${filename}`, {
        expiresIn: Math.floor((expiresIn || 3600000) / 1000), // Convert to seconds
      });
 
      return {
        success: true,
        url,
        message: `URL expires in ${expiresIn || 3600000}ms`
      };
    }
 
    // Delete
    const file = s3.file(`documents/${filename}`);
    await file.delete();
 
    return {
      success: true,
      message: 'File deleted'
    };
  }
});
 
export default agent;

Key Points:

  • Store files with metadata and content type
  • Generate time-limited public URLs for secure sharing
  • Support for custom content types and metadata
  • Delete returns boolean indicating if file existed

Agent-to-Agent Communication

Calling other agents to build workflows:

import { createAgent } from '@agentuity/runtime';
import enrichmentAgent from '@agent/enrichment';
import analyzerAgent from '@agent/analyzer';
import categorizerAgent from '@agent/categorizer';
import processorAgent from '@agent/processor';
 
const agent = createAgent('Workflow', {
  handler: async (ctx, input) => {
    ctx.logger.info('Starting multi-agent workflow');
 
    // Call enrichment agent
    const enriched = await enrichmentAgent.run({
      text: input.text
    });
 
    // Call multiple agents in parallel
    const [analyzed, categorized] = await Promise.all([
      analyzerAgent.run({ data: enriched }),
      categorizerAgent.run({ data: enriched })
    ]);
 
    // Call final processing agent with combined results
    const final = await processorAgent.run({
      analyzed,
      categorized,
      original: input
    });
 
    return {
      processed: true,
      results: final
    };
  }
});
 
export default agent;

Key Points:

  • Import and call agents: import agent from '@agent/name'; agent.run()
  • Type-safe when agents have schemas
  • Supports parallel execution with Promise.all()
  • Results automatically validated against output schemas

RAG Agent

Complete RAG (Retrieval-Augmented Generation) pattern:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
 
const agent = createAgent('RAG', {
  schema: {
    input: z.object({
      question: z.string()
    }),
    output: z.object({
      answer: z.string(),
      sources: z.array(z.string()),
      confidence: z.number()
    })
  },
  handler: async (ctx, input) => {
    // Search vector database for relevant context
    const results = await ctx.vector.search('knowledge-base', {
      query: input.question,
      limit: 3,
      similarity: 0.7
    });
 
    if (results.length === 0) {
      return {
        answer: 'I could not find relevant information to answer your question.',
        sources: [],
        confidence: 0
      };
    }
 
    // Build context from search results
    const context = results
      .map((r, i) => `[${i + 1}] ${r.metadata?.text}`)
      .join('\n\n');
 
    // Generate answer using LLM
    const { text } = await generateText({
      model: openai('gpt-5-nano'),
      prompt: `Answer the question based only on the provided context.
 
Context:
${context}
 
Question: ${input.question}
 
Provide a clear, concise answer citing the source numbers when appropriate.`
    });
 
    // Calculate confidence from average similarity
    const avgSimilarity = results.reduce((sum, r) => sum + r.similarity, 0) / results.length;
 
    return {
      answer: text,
      sources: results.map(r => r.id),
      confidence: avgSimilarity
    };
  }
});
 
export default agent;

Key Points:

  • Combines vector search with LLM generation
  • Provides source citations for verification
  • Confidence score based on retrieval quality
  • Graceful handling of no-results case

Error Handling Patterns

Comprehensive error handling with custom error classes:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
// Custom error classes
class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}
 
class ResourceNotFoundError extends Error {
  constructor(resourceId: string) {
    super(`Resource not found: ${resourceId}`);
    this.name = 'ResourceNotFoundError';
  }
}
 
const agent = createAgent('ResourceProcessor', {
  schema: {
    input: z.object({
      resourceId: z.string()
    }),
    output: z.object({
      message: z.string(),
      result: z.any().optional(),
      error: z.string().optional()
    })
  },
  handler: async (ctx, input) => {
    try {
      ctx.logger.info(`Processing resource: ${input.resourceId}`);
 
      // Simulate resource lookup
      const resource = await lookupResource(input.resourceId, ctx);
 
      // Process the resource
      const result = await processResource(resource, ctx);
 
      return {
        message: 'Resource processed successfully',
        result
      };
    } catch (error) {
      // Handle different error types
      if (error instanceof ValidationError) {
        ctx.logger.warn(`Validation error: ${error.message}`);
        return {
          error: 'Validation error',
          message: error.message
        };
      }
 
      if (error instanceof ResourceNotFoundError) {
        ctx.logger.warn(`Resource not found: ${error.message}`);
        return {
          error: 'Resource not found',
          message: error.message
        };
      }
 
      // Handle unexpected errors
      ctx.logger.error('Unexpected error', error);
      return {
        error: 'Internal server error',
        message: 'An unexpected error occurred'
      };
    }
  }
});
 
// Helper functions
async function lookupResource(resourceId: string, ctx: any) {
  const result = await ctx.kv.get('resources', resourceId);
 
  if (!result.exists) {
    throw new ResourceNotFoundError(resourceId);
  }
 
  return result.data;
}
 
async function processResource(resource: any, ctx: any) {
  ctx.logger.debug('Processing resource', resource);
 
  return {
    id: resource.id,
    status: 'processed',
    timestamp: new Date().toISOString()
  };
}
 
export default agent;

Key Points:

  • Custom error classes for specific scenarios
  • Structured error handling with try/catch
  • Different responses for different error types
  • Comprehensive logging at appropriate levels

Advanced Patterns

Session & Thread State Management

Managing state at three different scopes:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
const agent = createAgent('StateManager', {
  schema: {
    input: z.object({
      message: z.string(),
      resetThread: z.boolean().optional()
    }),
    output: z.object({
      response: z.string(),
      stats: z.object({
        requestDuration: z.number(),
        threadMessages: z.number(),
        sessionTotal: z.number()
      })
    })
  },
  handler: async (ctx, input) => {
    // Request-scoped state (cleared after request)
    ctx.state.set('startTime', Date.now());
 
    // Thread-scoped state (conversation context)
    if (input.resetThread) {
      await ctx.thread.destroy();
    }
 
    const threadMessages = (ctx.thread.state.get('messageCount') as number) || 0;
    ctx.thread.state.set('messageCount', threadMessages + 1);
 
    const messages = (ctx.thread.state.get('messages') as string[]) || [];
    messages.push(input.message);
    ctx.thread.state.set('messages', messages);
 
    // Session-scoped state (user-level, spans threads)
    const sessionTotal = (ctx.session.state.get('totalRequests') as number) || 0;
    ctx.session.state.set('totalRequests', sessionTotal + 1);
    ctx.session.state.set('lastActive', Date.now());
 
    return {
      response: `Processed message ${threadMessages + 1} in this conversation`,
      stats: {
        requestDuration: Date.now() - (ctx.state.get('startTime') as number),
        threadMessages: threadMessages + 1,
        sessionTotal: sessionTotal + 1
      }
    };
  }
});
 
export default agent;

Key Points:

  • Request state: Temporary data within a single request
  • Thread state: Conversation context across multiple requests
  • Session state: User-level data spanning multiple threads
  • Thread can be destroyed to reset conversation context

Event System & Lifecycle Hooks

Monitoring agent execution with events:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
const agent = createAgent('TaskProcessor', {
  schema: {
    input: z.object({ task: z.string() }),
    output: z.object({ result: z.string() })
  },
  handler: async (ctx, input) => {
    ctx.logger.info('Processing task', { task: input.task });
 
    // Simulate processing
    await new Promise(resolve => setTimeout(resolve, 100));
 
    return { result: `Completed: ${input.task}` };
  }
});
 
// Agent lifecycle events
agent.addEventListener('started', (eventName, agent, ctx) => {
  ctx.logger.info('Agent started', {
    sessionId: ctx.sessionId
  });
});
 
agent.addEventListener('completed', (eventName, agent, ctx) => {
  ctx.logger.info('Agent completed successfully', {
    sessionId: ctx.sessionId
  });
});
 
agent.addEventListener('errored', (eventName, agent, ctx, error) => {
  ctx.logger.error('Agent failed', {
    sessionId: ctx.sessionId,
    error: error.message
  });
});
 
export default agent;

App-level events can be added in app.ts:

import { createApp } from '@agentuity/runtime';
 
const app = createApp();
 
// Track all agent executions
app.addEventListener('agent.started', (eventName, agent, ctx) => {
  app.logger.info('Agent execution started', {
    session: ctx.sessionId
  });
});
 
app.addEventListener('agent.completed', (eventName, agent, ctx) => {
  app.logger.info('Agent execution completed', {
    session: ctx.sessionId
  });
});
 
export default app.server;

Key Points:

  • Agent-level events: started, completed, errored
  • App-level events track all agents globally
  • Session and thread events available for lifecycle tracking
  • Events useful for monitoring, analytics, and debugging

Multi-Agent Workflow

Orchestrating complex workflows with conditional logic:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import searchAgent from '@agent/search';
import deepAnalyzer from '@agent/deep-analyzer';
import basicAnalyzer from '@agent/basic-analyzer';
import recommendationEngine from '@agent/recommendation-engine';
 
const agent = createAgent('ResearchWorkflow', {
  schema: {
    input: z.object({
      query: z.string(),
      analysisDepth: z.enum(['basic', 'deep']),
      includeRecommendations: z.boolean().optional()
    }),
    output: z.object({
      searchResults: z.any(),
      analysis: z.any(),
      recommendations: z.array(z.string()).optional(),
      summary: z.string()
    })
  },
  handler: async (ctx, input) => {
    ctx.logger.info(`Starting workflow: ${input.query}`);
 
    // Step 1: Search for relevant information
    const searchResults = await searchAgent.run({
      query: input.query,
      limit: 10
    });
 
    // Step 2: Analyze results (conditional based on depth)
    const analysis = input.analysisDepth === 'deep'
      ? await deepAnalyzer.run({
          data: searchResults,
          includeDetails: true
        })
      : await basicAnalyzer.run({
          data: searchResults
        });
 
    // Step 3: Generate recommendations (optional)
    let recommendations;
    if (input.includeRecommendations) {
      const recResponse = await recommendationEngine.run({
        analysis,
        context: input.query
      });
      recommendations = recResponse.recommendations;
    }
 
    // Step 4: Create summary
    const resultCount = searchResults.count || 0;
    const summary = `Found ${resultCount} results, analyzed with ${input.analysisDepth} depth${
      recommendations ? `, generated ${recommendations.length} recommendations` : ''
    }`;
 
    ctx.logger.info('Workflow completed successfully');
 
    return {
      searchResults,
      analysis,
      recommendations,
      summary
    };
  }
});
 
export default agent;

Key Points:

  • Sequential agent orchestration
  • Conditional workflows based on input
  • Parallel execution where appropriate
  • Data passing between agents
  • Comprehensive logging for debugging

Background Tasks & Stream Creation

Using waitUntil for background processing and stream management:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
 
const agent = createAgent('BackgroundStreamer', {
  schema: {
    input: z.object({
      action: z.enum(['stream-with-background', 'multi-step-stream']),
      prompt: z.string().optional(),
      userId: z.string().optional(),
      includeAnalytics: z.boolean().optional()
    }),
    output: z.object({
      streamId: z.string().optional(),
      streamUrl: z.string().optional(),
      progressStreamId: z.string().optional(),
      progressStreamUrl: z.string().optional(),
      status: z.string().optional(),
      message: z.string().optional(),
      estimatedDuration: z.string().optional()
    })
  },
  handler: async (ctx, input) => {
    const { action, prompt, userId, includeAnalytics = true } = input;
 
    if (action === 'stream-with-background') {
      // Create a stream for the LLM response
      const stream = await ctx.stream.create('llm-response', {
        contentType: 'text/plain',
        metadata: {
          userId: userId || ctx.sessionId,
          model: 'gpt-5-nano',
          type: 'llm-generation'
        }
      });
 
      // Background task for streaming LLM response
      ctx.waitUntil(async () => {
        const { textStream } = streamText({
          model: openai('gpt-5-nano'),
          prompt: prompt || 'Tell me a short story'
        });
 
        await textStream.pipeTo(stream);
      });
 
      // Background task for analytics
      if (includeAnalytics && userId) {
        ctx.waitUntil(async () => {
          await logRequestAnalytics(userId, {
            action: 'stream_created',
            streamId: stream.id,
            timestamp: new Date()
          });
        });
      }
 
      // Background task for user activity tracking
      if (userId) {
        ctx.waitUntil(async () => {
          await updateUserActivity(userId, 'llm_stream_request');
        });
      }
 
      return {
        streamId: stream.id,
        streamUrl: stream.url,
        status: 'streaming',
        message: 'Stream created successfully'
      };
    }
 
    if (action === 'multi-step-stream') {
      const progressStream = await ctx.stream.create('progress', {
        contentType: 'application/json',
        metadata: {
          type: 'multi-step-progress',
          userId: userId || ctx.sessionId
        }
      });
 
      ctx.waitUntil(async () => {
        try {
          const steps = [
            { name: 'Analyzing input', duration: 1000 },
            { name: 'Generating response', duration: 2000 },
            { name: 'Post-processing', duration: 500 },
            { name: 'Finalizing', duration: 300 }
          ];
 
          for (let i = 0; i < steps.length; i++) {
            const step = steps[i];
 
            await progressStream.write(JSON.stringify({
              step: i + 1,
              total: steps.length,
              name: step.name,
              progress: ((i + 1) / steps.length) * 100,
              timestamp: new Date().toISOString()
            }) + '\n');
 
            await new Promise(resolve => setTimeout(resolve, step.duration));
          }
 
          await progressStream.write(JSON.stringify({
            completed: true,
            message: 'All steps completed successfully',
            timestamp: new Date().toISOString()
          }) + '\n');
        } finally {
          await progressStream.close();
        }
      });
 
      return {
        progressStreamId: progressStream.id,
        progressStreamUrl: progressStream.url,
        estimatedDuration: '3-4 seconds'
      };
    }
 
    return { message: 'Invalid action' };
  }
});
 
// Helper functions
async function logRequestAnalytics(userId: string, data: any) {
  console.log(`Analytics for user ${userId}:`, data);
}
 
async function updateUserActivity(userId: string, activity: string) {
  console.log(`Updated activity for user ${userId}: ${activity}`);
}
 
export default agent;

Key Points:

  • waitUntil() executes tasks after response is sent
  • Multiple background tasks can run concurrently
  • Streams support metadata for organization
  • Manual stream writing for progress updates
  • Always close streams in finally blocks

Specialized Routes & Patterns

Email Route Handler

Handling incoming emails with automatic parsing:

import { createRouter } from '@agentuity/runtime';
import emailProcessor from '@agent/email-processor';
 
const router = createRouter();
 
router.email('support@example.com', async (email, c) => {
  // Process email with agent
  const result = await emailProcessor.run({
    sender: email.fromEmail() || 'unknown',
    subject: email.subject() || 'no subject',
    content: email.text() || email.html() || ''
  });
 
  return c.json({
    processed: true,
    ticketId: result.ticketId
  });
});
 
export default router;

Key Points:

  • Emails automatically parsed from RFC822 format
  • Access to text, HTML, and attachments
  • Route-level context provides agent access

WebSocket Real-Time Chat

Creating WebSocket endpoints for bidirectional communication:

import { createRouter } from '@agentuity/runtime';
import chatAgent from '@agent/chat';
 
const router = createRouter();
 
router.websocket('/chat', (c) => (ws) => {
  ws.onOpen(() => {
    ws.send(JSON.stringify({ type: 'connected' }));
  });
 
  ws.onMessage(async (event) => {
    const message = JSON.parse(event.data);
 
    // Process with agent
    const response = await chatAgent.run({
      message: message.text
    });
 
    ws.send(JSON.stringify({
      type: 'response',
      data: response
    }));
  });
 
  ws.onClose(() => {
    ctx.logger.info('Client disconnected');
  });
});
 
export default router;

Key Points:

  • WebSocket lifecycle: onOpen, onMessage, onClose
  • Bidirectional real-time communication
  • Agent integration for message processing

Cron Scheduled Jobs

Scheduling recurring tasks with cron syntax:

import { createRouter } from '@agentuity/runtime';
import reportGenerator from '@agent/report-generator';
import notificationAgent from '@agent/notification';
import healthCheck from '@agent/health-check';
 
const router = createRouter();
 
// Daily report at 9 AM
router.cron('0 9 * * *', async (c) => {
  ctx.logger.info('Running daily report generation');
 
  const report = await reportGenerator.run({
    type: 'daily',
    date: new Date().toISOString(),
    includeMetrics: true
  });
 
  // Store report
  await ctx.kv.set('reports', `daily-${Date.now()}`, report);
 
  // Send notification
  await notificationAgent.run({
    type: 'email',
    subject: 'Daily Report Ready',
    recipients: ['admin@example.com'],
    reportId: report.id
  });
 
  return c.json({ success: true, reportId: report.id });
});
 
// Health check every 5 minutes
router.cron('*/5 * * * *', async (c) => {
  const health = await healthCheck.run({});
 
  if (health.status !== 'ok') {
    ctx.logger.warn('Health check failed', { status: health.status });
  }
 
  return c.json({ healthy: health.status === 'ok' });
});
 
export default router;

Cron schedule format:

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Key Points:

  • Cron schedules defined in code, not UI
  • Use standard cron syntax
  • Access to full agent context
  • Useful for reports, cleanup, monitoring

Server-Sent Events for Progress

Streaming progress updates to clients:

import { createRouter } from '@agentuity/runtime';
import processor from '@agent/processor';
 
const router = createRouter();
 
router.sse('/updates', (c) => async (stream) => {
  // Send initial connection
  await stream.write({ type: 'connected' });
 
  // Process task and send updates
  const updates = await processor.run({ task: 'process' });
 
  for (const update of updates) {
    await stream.write({
      type: 'progress',
      data: update
    });
  }
 
  await stream.write({ type: 'complete' });
 
  stream.onAbort(() => {
    ctx.logger.info('Client disconnected');
  });
});
 
export default router;

Key Points:

  • Server-to-client streaming only (one-way)
  • Automatic reconnection on client side
  • onAbort called when client disconnects
  • Use for progress tracking, live updates

For complete routing documentation including HTTP methods, route parameters, query strings, validation with zValidator, and additional route types like Stream and SMS, see the Routing & Triggers Guide.

Advanced Patterns

Agent Evaluations

Automatically testing agent outputs for quality:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
const agent = createAgent('QueryProcessor', {
  schema: {
    input: z.object({ query: z.string() }),
    output: z.object({
      result: z.string(),
      confidence: z.number()
    })
  },
  handler: async (ctx, input) => {
    // Process query and generate result
    const result = await processQuery(input.query);
 
    return {
      result,
      confidence: 0.85 // Confidence score based on processing
    };
  }
});
 
// Basic evaluation: confidence threshold check
agent.createEval('confidence-check', {
  description: 'Ensures confidence score meets minimum threshold',
  handler: async (ctx, input, output) => {
    const passed = output.confidence >= 0.8;
 
    return {
      success: true,
      passed,
      metadata: {
        confidence: output.confidence,
        threshold: 0.8,
        reason: passed ? 'Sufficient confidence' : 'Low confidence'
      }
    };
  }
});
 
async function processQuery(query: string) {
  // Your processing logic here
  return `Processed: ${query}`;
}
 
export default agent;

Key Points:

  • Evals run automatically after agent completes
  • Non-blocking execution via waitUntil()
  • Access to input, output, and full context
  • Pass/fail pattern with metadata

For comprehensive evaluation patterns including LLM-as-judge, hallucination detection, RAG quality metrics (contextual relevancy, answer relevancy, faithfulness), and A/B testing, see the Evaluations Guide.

Telemetry & Distributed Tracing

Using OpenTelemetry for observability:

import { createAgent } from '@agentuity/runtime';
import { SpanStatusCode } from '@opentelemetry/api';
 
const agent = createAgent('TracedProcessor', {
  handler: async (ctx, input) => {
    return ctx.tracer.startActiveSpan('process-request', async (span) => {
      try {
        // Add attributes to the span
        span.setAttribute('data.type', typeof input);
 
        // Create a child span for data processing
        return await ctx.tracer.startActiveSpan('process-data', async (childSpan) => {
          try {
            // Add event to the span
            childSpan.addEvent('processing-started', {
              timestamp: Date.now()
            });
 
            // Simulate data processing
            const result = await processData(input);
 
            // Add event to the span
            childSpan.addEvent('processing-completed', {
              timestamp: Date.now(),
              resultSize: JSON.stringify(result).length
            });
 
            // Set span status
            childSpan.setStatus({ code: SpanStatusCode.OK });
 
            return result;
          } catch (error) {
            // Record exception in the span
            childSpan.recordException(error as Error);
            childSpan.setStatus({
              code: SpanStatusCode.ERROR,
              message: (error as Error).message
            });
 
            throw error;
          } finally {
            childSpan.end();
          }
        });
      } catch (error) {
        // Record exception in the parent span
        span.recordException(error as Error);
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: (error as Error).message
        });
 
        ctx.logger.error('Error processing request', error);
        throw error;
      } finally {
        span.end();
      }
    });
  }
});
 
async function processData(data: any) {
  await new Promise(resolve => setTimeout(resolve, 100));
 
  return {
    processed: true,
    input: data,
    timestamp: new Date().toISOString()
  };
}
 
export default agent;

Key Points:

  • Hierarchical spans (parent/child relationships)
  • Span attributes for filtering and analysis
  • Event recording for granular tracking
  • Exception recording for error analysis
  • OpenTelemetry standard for distributed tracing

Streaming Examples

OpenAI Streaming

Streaming LLM responses with the Vercel AI SDK:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
 
const agent = createAgent('OpenAIStreamer', {
  schema: {
    input: z.object({
      prompt: z.string()
    }),
    stream: true
  },
  handler: async (ctx, input) => {
    const { textStream } = streamText({
      model: openai('gpt-5-nano'),
      prompt: input.prompt
    });
 
    return textStream;
  }
});
 
export default agent;

Key Points:

  • stream: true in schema enables streaming
  • Direct return of stream from Vercel AI SDK
  • Responsive user experience with real-time output

Dependencies:

bun add ai @ai-sdk/openai

Agent-to-Agent Streaming

Calling another agent and streaming its response:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
import historyExpert from '@agent/history-expert';
 
const agent = createAgent('StreamingRouter', {
  schema: {
    input: z.object({
      question: z.string()
    }),
    stream: true
  },
  handler: async (ctx, input) => {
    // Call the expert agent and stream its response
    const response = await historyExpert.run({
      question: input.question
    });
 
    return response;
  }
});
 
export default agent;

Key Points:

  • Stream responses pass through seamlessly
  • Enables agent chaining with streaming
  • Maintains end-to-end streaming experience

Learn More:

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!