building-ai-agent-on-cloudflare

Installation
SKILL.md

Building Cloudflare Agents

Creates AI-powered agents using Cloudflare's Agents SDK with persistent state, real-time communication, and tool integration.

When to Use

  • User wants to build an AI agent or chatbot
  • User needs stateful, real-time AI interactions
  • User asks about the Cloudflare Agents SDK
  • User wants scheduled tasks or background AI work
  • User needs WebSocket-based AI communication

Prerequisites

  • Cloudflare account with Workers enabled
  • Node.js 18+ and npm/pnpm/yarn
  • Wrangler CLI (npm install -g wrangler)

Quick Start

npm create cloudflare@latest -- my-agent --template=cloudflare/agents-starter
cd my-agent
npm start

Agent runs at http://localhost:8787

Core Concepts

What is an Agent?

An Agent is a stateful, persistent AI service that:

  • Maintains state across requests and reconnections
  • Communicates via WebSockets or HTTP
  • Runs on Cloudflare's edge via Durable Objects
  • Can schedule tasks and call tools
  • Scales horizontally (each user/session gets own instance)

Agent Lifecycle

Client connects → Agent.onConnect() → Agent processes messages
                                    → Agent.onMessage()
                                    → Agent.setState() (persists + syncs)
Client disconnects → State persists → Client reconnects → State restored

Basic Agent Structure

import { Agent, Connection } from 'agents';

interface Env {
  AI: Ai; // Workers AI binding
}

interface State {
  messages: Array<{ role: string; content: string }>;
  preferences: Record<string, string>;
}

export class MyAgent extends Agent<Env, State> {
  // Initial state for new instances
  initialState: State = {
    messages: [],
    preferences: {},
  };

  // Called when agent starts or resumes
  async onStart() {
    console.log('Agent started with state:', this.state);
  }

  // Handle WebSocket connections
  async onConnect(connection: Connection) {
    connection.send(
      JSON.stringify({
        type: 'welcome',
        history: this.state.messages,
      })
    );
  }

  // Handle incoming messages
  async onMessage(connection: Connection, message: string) {
    const data = JSON.parse(message);

    if (data.type === 'chat') {
      await this.handleChat(connection, data.content);
    }
  }

  // Handle disconnections
  async onClose(connection: Connection) {
    console.log('Client disconnected');
  }

  // React to state changes
  onStateUpdate(state: State, source: string) {
    console.log('State updated by:', source);
  }

  private async handleChat(connection: Connection, userMessage: string) {
    // Add user message to history
    const messages = [...this.state.messages, { role: 'user', content: userMessage }];

    // Call AI
    const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', {
      messages,
    });

    // Update state (persists and syncs to all clients)
    this.setState({
      ...this.state,
      messages: [...messages, { role: 'assistant', content: response.response }],
    });

    // Send response
    connection.send(
      JSON.stringify({
        type: 'response',
        content: response.response,
      })
    );
  }
}

Entry Point Configuration

// src/index.ts
import { routeAgentRequest } from 'agents';
import { MyAgent } from './agent';

export default {
  async fetch(request: Request, env: Env) {
    // routeAgentRequest handles routing to /agents/:class/:name
    return (await routeAgentRequest(request, env)) || new Response('Not found', { status: 404 });
  },
};

export { MyAgent };

Clients connect via: wss://my-agent.workers.dev/agents/MyAgent/session-id

Wrangler Configuration

name = "my-agent"
main = "src/index.ts"
compatibility_date = "2024-12-01"

[ai]
binding = "AI"

[durable_objects]
bindings = [{ name = "AGENT", class_name = "MyAgent" }]

[[migrations]]
tag = "v1"
new_classes = ["MyAgent"]

State Management

Reading State

// Current state is always available
const currentMessages = this.state.messages;
const userPrefs = this.state.preferences;

Updating State

// setState persists AND syncs to all connected clients
this.setState({
  ...this.state,
  messages: [...this.state.messages, newMessage],
});

// Partial updates work too
this.setState({
  preferences: { ...this.state.preferences, theme: 'dark' },
});

SQL Storage

For complex queries, use the embedded SQLite database:

// Create tables
await this.sql`
  CREATE TABLE IF NOT EXISTS documents (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`;

// Insert
await this.sql`
  INSERT INTO documents (title, content)
  VALUES (${title}, ${content})
`;

// Query
const docs = await this.sql`
  SELECT * FROM documents WHERE title LIKE ${`%${search}%`}
`;

Scheduled Tasks

Agents can schedule future work:

async onMessage(connection: Connection, message: string) {
  const data = JSON.parse(message);

  if (data.type === "schedule_reminder") {
    // Schedule task for 1 hour from now
    const { id } = await this.schedule(3600, "sendReminder", {
      message: data.reminderText,
      userId: data.userId,
    });

    connection.send(JSON.stringify({ type: "scheduled", taskId: id }));
  }
}

// Called when scheduled task fires
async sendReminder(data: { message: string; userId: string }) {
  // Send notification, email, etc.
  console.log(`Reminder for ${data.userId}: ${data.message}`);

  // Can also update state
  this.setState({
    ...this.state,
    lastReminder: new Date().toISOString(),
  });
}

Schedule Options

// Delay in seconds
await this.schedule(60, 'taskMethod', { data });

// Specific date
await this.schedule(new Date('2025-01-01T00:00:00Z'), 'taskMethod', { data });

// Cron expression (recurring)
await this.schedule('0 9 * * *', 'dailyTask', {}); // 9 AM daily
await this.schedule('*/5 * * * *', 'everyFiveMinutes', {}); // Every 5 min

// Manage schedules
const schedules = await this.getSchedules();
await this.cancelSchedule(taskId);

Chat Agent (AI-Powered)

For chat-focused agents, extend AIChatAgent:

import { AIChatAgent } from 'agents/ai-chat-agent';

export class ChatBot extends AIChatAgent<Env> {
  // Called for each user message
  async onChatMessage(message: string) {
    const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', {
      messages: [
        { role: 'system', content: 'You are a helpful assistant.' },
        ...this.messages, // Automatic history management
        { role: 'user', content: message },
      ],
      stream: true,
    });

    // Stream response back to client
    return response;
  }
}

Features included:

  • Automatic message history
  • Resumable streaming (survives disconnects)
  • Built-in saveMessages() for persistence

Client Integration

React Hook

import { useAgent } from 'agents/react';

function Chat() {
  const { state, send, connected } = useAgent({
    agent: 'my-agent',
    name: userId, // Agent instance ID
  });

  const sendMessage = (text: string) => {
    send(JSON.stringify({ type: 'chat', content: text }));
  };

  return (
    <div>
      {state.messages.map((msg, i) => (
        <div key={i}>
          {msg.role}: {msg.content}
        </div>
      ))}
      <input onKeyDown={(e) => e.key === 'Enter' && sendMessage(e.target.value)} />
    </div>
  );
}

Vanilla JavaScript

const ws = new WebSocket('wss://my-agent.workers.dev/agents/MyAgent/user123');

ws.onopen = () => {
  console.log('Connected to agent');
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};

ws.send(JSON.stringify({ type: 'chat', content: 'Hello!' }));

Common Patterns

See references/agent-patterns.md for:

  • Tool calling and function execution
  • Multi-agent orchestration
  • RAG (Retrieval Augmented Generation)
  • Human-in-the-loop workflows

Deployment

# Deploy
npx wrangler deploy

# View logs
wrangler tail

# Test endpoint
curl https://my-agent.workers.dev/agents/MyAgent/test-user

Troubleshooting

See references/troubleshooting.md for common issues.

References

Related skills

More from involvex/aetheris

Installs
1
First Seen
Mar 26, 2026