Hands-on · Module 09 · Intermediate

Build an agent with the OpenAI & Anthropic APIs

Sometimes you need code. When the agent lives inside your app, on your server, or on Cloudflare, you'll write it in JavaScript or Python. This module gets you from zero to a working agent in about 80 lines.

The shape of every code agent

All coded agents follow the same three-part structure:

  1. Define tools — JSON schemas the model can call.
  2. The loop — send messages, execute tool calls, feed results back.
  3. Termination — stop when the model returns a final answer (or after N iterations).

OpenAI: a working agent (Node.js)

This one takes a client website URL and returns a short accessibility audit. It uses two tools: fetch_url to grab HTML and check_contrast to test color pairs.

Node.js · openai v5import OpenAI from "openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// 1. Define tools
const tools = [
  {
    type: "function",
    function: {
      name: "fetch_url",
      description: "Fetch a URL and return its HTML.",
      parameters: {
        type: "object",
        properties: { url: { type: "string" } },
        required: ["url"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "check_contrast",
      description: "Return the WCAG contrast ratio between two hex colors.",
      parameters: {
        type: "object",
        properties: {
          fg: { type: "string", description: "foreground hex" },
          bg: { type: "string", description: "background hex" }
        },
        required: ["fg", "bg"]
      }
    }
  }
];

// 2. Implement the tools
async function runTool(name, args) {
  if (name === "fetch_url") {
    const r = await fetch(args.url);
    return (await r.text()).slice(0, 40000);
  }
  if (name === "check_contrast") {
    return { ratio: contrast(args.fg, args.bg) };
  }
}

// 3. The agent loop
async function agent(goal) {
  const messages = [
    {
      role: "system",
      content: "You are an accessibility auditor. Fetch the URL, find color pairs in the HTML, check WCAG contrast for each, and return a short report with pass/fail."
    },
    { role: "user", content: goal }
  ];

  for (let i = 0; i < 10; i++) {
    const resp = await openai.chat.completions.create({
      model: "gpt-5-mini",
      messages,
      tools
    });
    const msg = resp.choices[0].message;
    messages.push(msg);

    if (!msg.tool_calls?.length) return msg.content;

    for (const call of msg.tool_calls) {
      const args = JSON.parse(call.function.arguments);
      const result = await runTool(call.function.name, args);
      messages.push({
        role: "tool",
        tool_call_id: call.id,
        content: JSON.stringify(result)
      });
    }
  }
  throw new Error("Agent exceeded max iterations");
}

const report = await agent("Audit https://example.com for color contrast.");
console.log(report);

Anthropic: the same agent in Claude

Same shape, different SDK. Anthropic uses tool_use and tool_result content blocks instead of tool call arrays.

Node.js · @anthropic-ai/sdkimport Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const tools = [
  {
    name: "fetch_url",
    description: "Fetch a URL and return its HTML.",
    input_schema: {
      type: "object",
      properties: { url: { type: "string" } },
      required: ["url"]
    }
  }
];

async function agent(goal) {
  const messages = [{ role: "user", content: goal }];

  for (let i = 0; i < 10; i++) {
    const resp = await client.messages.create({
      model: "claude-sonnet-4-5",
      max_tokens: 2048,
      system: "You are an accessibility auditor. Use tools; end with a written report.",
      tools,
      messages
    });

    messages.push({ role: "assistant", content: resp.content });

    if (resp.stop_reason !== "tool_use") {
      return resp.content.find(c => c.type === "text")?.text;
    }

    const toolResults = [];
    for (const block of resp.content) {
      if (block.type === "tool_use") {
        const out = await runTool(block.name, block.input);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: JSON.stringify(out)
        });
      }
    }
    messages.push({ role: "user", content: toolResults });
  }
}

Cloudflare Agents SDK: stateful agents on the edge

The Cloudflare Agents SDK gives you persistent state, WebSocket streaming, and a built-in scheduler — perfect for agents that need to remember users across sessions.

TypeScript · Cloudflare Agentsimport { Agent } from "agents";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

export class EstimatorAgent extends Agent<Env> {
  // Runs when a client connects
  async onMessage(message: string, conn) {
    const history = await this.state.get("history") ?? [];
    history.push({ role: "user", content: message });

    const stream = streamText({
      model: openai("gpt-5-mini"),
      messages: history,
      tools: this.tools
    });

    for await (const chunk of stream.textStream) conn.send(chunk);

    history.push({ role: "assistant", content: await stream.text });
    await this.state.set("history", history);
  }

  // Runs on a schedule you set with this.schedule()
  async followUp(clientEmail: string) {
    await this.sendEmail(clientEmail, "Checking in on your estimate…");
  }
}

Each user gets their own EstimatorAgent instance (a Durable Object under the hood). State survives restarts. Perfect for a client-facing "estimate my project" chatbot on your marketing site.

Reliability tricks that actually matter

  • Structured outputs. Force JSON when a downstream step consumes the result.
  • Retries with backoff. Wrap every API call. Assume 1 in 100 calls will time out.
  • Max iterations. Every loop needs a safety cap. 10 is a fine default.
  • Timeouts on tool calls. A hanging fetch_url will freeze the whole agent.
  • Log every tool call and every model response. You cannot debug what you can't see.
  • Small, sharp system prompts. If you can't explain the agent's job in 3 sentences, it will fail.
Build · 45 min

Ship your first API agent

Copy the OpenAI code above into a Node file. Give it an OPENAI_API_KEY and run it against a client site. Then, one at a time:

  1. Add a third tool of your own (e.g., check_alt_text).
  2. Add a max-iteration counter with a friendly error.
  3. Swap in Claude Sonnet using the Anthropic template.
  4. Wrap the whole thing in a Cloudflare Worker so you can hit it from the browser.
Or pass the quiz above.