Most “build an AI agent” tutorials show you a single API call with a clever prompt and call it a day. That’s not an agent — that’s a chat completion. A real agent can decide it needs more information, go fetch that information itself, and keep working until the task is actually done, without you hand-coding every branch of that logic.

This guide walks through exactly how to build one using the Claude API: the system prompt that defines its job, the tools it can call, and the loop that ties it all together in Node.js.

What Actually Makes Something an “AI Agent”

Two ingredients turn a plain API call into an agent:

  1. A system prompt that defines the agent’s role, its hard rules, and when it’s required to use a tool instead of guessing.
  2. Tools — functions you describe to Claude, that Claude can choose to call mid-response instead of answering directly.

When Claude decides it needs a tool, the API doesn’t run the function for you. It returns control to your code with stop_reason: "tool_use" and the exact function name and arguments to run. You execute it, hand the result back, and call the API again. That request-execute-respond loop, repeated until Claude has what it needs, is the entire mechanism behind an “agent.”

Prerequisites

  • Node.js 18+
  • An API key from the Claude Console
  • The official SDK: @anthropic-ai/sdk

Step 1: Set Up the Project

bash

mkdir claude-agent && cd claude-agent
npm init -y
npm install @anthropic-ai/sdk dotenv
echo "ANTHROPIC_API_KEY=sk-ant-your-key-here" > .env

Step 2: Write a System Prompt That Actually Defines the Job

This is the part most tutorials skip. The system prompt is a dedicated system parameter on the request — not a chat message — and it’s the single biggest lever you have over how the agent behaves. A good one states the role, the guardrails, and exactly when it must use a tool instead of assuming an answer:

js

const SYSTEM_PROMPT = `You are OrderBot, a customer support agent for an e-commerce store.

Rules:
- Always call lookup_order before answering anything about an order's status or contents.
- Never invent an order ID, tracking number, or refund amount.
- If the customer didn't include an order ID, ask for one instead of guessing.
- Keep replies under 3 sentences and end with a clear next step.`;

Step 3: Define Your Tools

Each tool needs a name, a description, and an input_schema (plain JSON Schema). The description is what Claude reads to decide when to call the tool — vague descriptions are the #1 reason agents misfire, so be explicit about what the tool does and when to use it.

js

const tools = [
  {
    name: "lookup_order",
    description:
      "Fetch the current status, items, and delivery date for an order using its order ID. Call this before answering any question about an order's status.",
    input_schema: {
      type: "object",
      properties: {
        orderId: { type: "string", description: "e.g. ORD-10293" }
      },
      required: ["orderId"]
    }
  },
  {
    name: "check_refund_eligibility",
    description:
      "Check whether an order qualifies for a refund. Only call this after lookup_order has confirmed the order exists.",
    input_schema: {
      type: "object",
      properties: { orderId: { type: "string" } },
      required: ["orderId"]
    }
  }
];

// Swap these for real MongoDB/API calls in production
async function lookupOrder(orderId) {
  return { orderId, status: "Shipped", eta: "2 days", items: ["Wireless Mouse"] };
}
async function checkRefundEligibility(orderId) {
  return { orderId, eligible: true, reason: "Within 30-day window" };
}
async function runTool(name, input) {
  if (name === "lookup_order") return lookupOrder(input.orderId);
  if (name === "check_refund_eligibility") return checkRefundEligibility(input.orderId);
  throw new Error(`Unknown tool: ${name}`);
}

Step 4: Build the Core Agent Loop

This is the exact workflow that makes it an agent: send the message, check stop_reason, execute any tool Claude asked for, feed the result back with a matching tool_use_id, and repeat until Claude stops asking for tools.

js

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

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

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-5",
      max_tokens: 1024,
      system: SYSTEM_PROMPT,
      tools,
      messages
    });

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

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

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

runAgent("Where's my order ORD-10293?").then(console.log);

Step 5: Let It Chain Tools on Its Own

Because every result gets fed back and the API is called again, Claude can chain steps you never explicitly coded — look up the order, decide refund eligibility needs checking, call that second tool, then answer. That branching comes entirely from the tool descriptions and the loop, not from if/else logic you wrote. The loop above already handles multiple tool calls in a single turn, since it iterates over every tool_use block in the response.

Pro Tip: Make It Production-Ready With a Queue

Don’t run this loop synchronously inside an Express request handler — a multi-step tool chain can take several seconds, and any one call can fail. Push each agent job onto a Redis-backed queue (BullMQ works well), let a worker run runAgent with retries and backoff, and persist the final response so the client can poll it or get notified via webhook. That keeps a slow agent from holding an HTTP connection open.

Common Mistakes to Avoid

  • Vague tool descriptions — Claude either ignores the tool or misuses it.
  • Forgetting to push the assistant’s response into messages before adding the tool result — this breaks context on the next call.
  • Mismatched tool_use_id — the result must reference the exact ID Claude sent.
  • No iteration cap — add a max-loop guard so a flaky tool can’t spin forever.
  • Throwing inside runTool — catch errors and return them as the tool result content instead, so Claude can react and retry or ask the user.

FAQ

Do I need LangChain or another framework to build a Claude agent? No. The SDK plus the loop above is the whole mechanism — frameworks just wrap this same pattern.

Which model should I use? claude-sonnet-5 is the default choice for most agents; reach for claude-opus-5 on more ambiguous, multi-step reasoning tasks.

Can an agent run without a system prompt? Technically yes, but it’ll be inconsistent about when to call tools versus guess — the system prompt is what makes behavior reliable.

Wrapping Up

An “AI agent” isn’t a special model or a magic prompt — it’s a system prompt that sets the rules, a set of well-described tools, and a loop that keeps handing control back to Claude until the job is done. Start with the pattern above, swap in your own tools, and you have a working, production-shaped agent.

Written by

’m Nilesh, a Software Development Engineer with 2+ years of experience, specializing in Go, JavaScript, Python, Docker, Kubernetes, Git, Jenkins, microservices, and system design (LLD/HLD), backed by a strong foundation in data structures and algorithms. Alongside my engineering journey, I bring 4+ years of hands-on experience in SEO, where I’ve worked extensively on content strategy, keyword research, technical SEO, and organic growth, helping products and businesses scale efficiently by aligning solid technology with search-driven performance.