---
title: "How to add agent analytics to a Claude agent with tool use"
description: "Run Claude's tool_use loop through AmplitudeToolLoop in TypeScript and track every response, tool call, token, and cost per agent session."
canonical: "https://amplitude.com/en-us/build/anthropic-agent-analytics"
---

# How to add agent analytics to a Claude agent with tool use

Run Claude's tool_use loop through AmplitudeToolLoop in TypeScript and track every response, tool call, token, and cost per agent session.

Source: https://amplitude.com/en-us/build/anthropic-agent-analytics

---

# How to add agent analytics to a Claude agent with tool use

Install the Amplitude AI SDK next to the Anthropic SDK, define two tools, run Claude's tool loop through AmplitudeToolLoop inside an agent session, and flush before the process exits. About 20 minutes, all copy-paste.

- Node.js 20.6+
- TypeScript
- Anthropic SDK
- Amplitude AI SDK
- 20 min

Chris Tam · Last updated 19 October 2026

Claude’s tool loop makes several model calls for one task. I let `AmplitudeToolLoop` record those calls and tool results, then give it a plain Anthropic client so [agent analytics](https://amplitude.com/agent-analytics) does not count each response twice.

I’ll build a TypeScript script with two sample support tools, cache-aware token counts, and an upload wait before exit. The Claude client adapter supplies the `max_tokens` value required by the Messages API.

A `runSupportAgent()` function that sends a support question to Claude with two tools, `get_order_status` and `search_help_docs`, and records `[Agent] User Message`, one `[Agent] AI Response` per model call, and one `[Agent] Tool Call` per tool run, all under one `[Agent] Session ID` per task and one `[Agent] Agent ID` (`support-agent`). Each AI Response carries `[Agent] Model Name`, `[Agent] Input Tokens`, `[Agent] Output Tokens`, `[Agent] Cost USD`, and `[Agent] Latency Ms`. By the end you'll see those events in your test user's event stream and the task in agent analytics.

## Prerequisites

- A Node.js 20.6 or later TypeScript project that runs as ES modules (`"type": "module"` in `package.json`).
- An Anthropic API key in `ANTHROPIC_API_KEY`.
- An Amplitude project with agent analytics enabled, and its project API key. In Amplitude, go to Settings > Projects > your project > General, then Manage next to API Key. Agent data can live in the same project as your product analytics, which is the simplest setup.
- The View Agent Analytics Objects permission on your role, which you need to open agent analytics at all.

No Amplitude account yet? A free account is enough for this tutorial; every plan includes a free allotment of agent sessions. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

These examples use a US-region Amplitude project. For an EU project, supply an [EU-configured analytics client](https://amplitude.com/docs/amplitude-ai/agent-analytics/privacy-modes#data-residency) instead of initializing with the API key alone.

## 1. Install the SDKs

The Node path needs `@amplitude/ai` and the Node analytics client it sends through. Provider packages are optional peer dependencies, so add `@anthropic-ai/sdk` yourself. `tsx` runs the TypeScript files directly. For every option, see the [Amplitude AI SDK reference](https://amplitude.com/docs/sdks/agent-analytics/sdk).

```sh
pnpm add @amplitude/ai @amplitude/analytics-node @anthropic-ai/sdk
pnpm add -D tsx typescript @types/node
```

.env

```sh
# .env
AMPLITUDE_AI_API_KEY=your-amplitude-project-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
```

Prefer to let a coding agent do the instrumentation? Tell it to fetch and follow `https://raw.githubusercontent.com/amplitude/Amplitude-AI-Node/main/amplitude-ai.md`, the instructions file Amplitude publishes for coding agents. The steps below are what it should produce for a Claude tool loop.

## 2. Initialize Amplitude and the Claude client once

Create one module that constructs the Amplitude client, the agent, and the Claude client at module scope. Everything else imports from it.

src/lib/amplitude.ts

```typescript
// src/lib/amplitude.ts
import Anthropic from '@anthropic-ai/sdk';
import { AIConfig, AmplitudeAI } from '@amplitude/ai';

const apiKey = process.env.AMPLITUDE_AI_API_KEY;

if (!apiKey) {
  throw new Error('AMPLITUDE_AI_API_KEY is not set');
}

export const ai = new AmplitudeAI({
  apiKey,
  config: new AIConfig({
    contentMode: 'full', // prompt and response text, with PII redaction
    redactPii: true,
    debug: process.env.NODE_ENV !== 'production',
  }),
});

export const supportAgent = ai.agent('support-agent', {
  description: 'Answers order and help-center questions with tools',
});

// Wait for a promise-returning client or the analytics return object's promise.
export async function flushAgentEvents(): Promise<void> {
  const pending = ai.flush();
  if (pending && typeof pending === 'object' && 'promise' in pending) {
    await pending.promise;
  } else {
    await pending;
  }

export const MODEL = 'claude-sonnet-5-5';

// The plain Anthropic client, not the wrapper from @amplitude/ai.
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

type LoopRequest = Omit<Anthropic.MessageCreateParamsNonStreaming, 'max_tokens'> & {
  max_tokens?: number;
};

// AmplitudeToolLoop calls client.messages.create for you. This client adds
// max_tokens, which the Messages API requires on every request.
export const claude = {
  messages: {
    create: (params: LoopRequest) =>
      anthropic.messages.create({ ...params, max_tokens: params.max_tokens ?? 4096 }),
  },
};
```

**Why.** The loop records every response and tool call itself, so give it the plain Anthropic client. A client wrapped with `new Anthropic({ apiKey, amplitude: ai })` would record each response a second time. The loop builds each request from the model, messages, tools, and system prompt you pass it, and the Messages API rejects a request without `max_tokens`, so the small `claude` adapter sets it. Thinking tokens count toward `max_tokens`, so leave room above the length of the answer you expect.

Keep the named agent at module scope so tasks reuse its configuration. `contentMode: 'full'` is the default and sends prompt and response text after scrubbing emails, phone numbers, SSNs, card numbers, and IP addresses. If tool inputs or outputs must stay in your infrastructure, use `'metadata_only'`: you still get tokens, latency, model, cost, and session grouping, with no text. `debug` prints each tracked event to stderr outside production.

Use the canonical model ID, `claude-sonnet-5-5`, not a gateway alias. Cost comes from token counts multiplied against a public model-price catalog, and a name the catalog can't price gets no cost.

Redaction matches patterns; it does not remove every kind of personal data. Review representative payloads before production capture. In `metadata_only`, built-in quality signals that need message text are unavailable.

## 3. Define the tools and the executor

Tool definitions follow Anthropic's schema: a `name`, a `description` that says what the tool does and when to use it, and an `input_schema` in JSON Schema. The executor is one function that receives the name and input of each `tool_use` block Claude returns and runs the matching code.

src/tools.ts

```typescript
// src/tools.ts
import type Anthropic from '@anthropic-ai/sdk';

export const tools = [
  {
    name: 'get_order_status',
    description:
      'Looks up the shipping status of one order by its order number. Use it when the user asks where an order is, when it ships, or whether it arrived. Returns the status, carrier, and estimated delivery date. It does not return payment or refund details.',
    input_schema: {
      type: 'object',
      properties: {
        order_id: { type: 'string', description: 'The order number, for example 1042.' },
      },
      required: ['order_id'],
    },
  {
    name: 'search_help_docs',
    description:
      'Searches the help center for articles about returns, refunds, shipping, and account settings. Use it for policy questions the order record cannot answer. Returns up to three matching articles with a title and body.',
    input_schema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'A short search phrase, for example "return window".' },
      },
      required: ['query'],
    },
] satisfies Anthropic.Tool[];

async function getOrderStatus(orderId: string) {
  // Replace with a call to your order service.
  return { order_id: orderId, status: 'shipped', carrier: 'UPS', estimated_delivery: '2026-10-18' };
}

async function searchHelpDocs(query: string) {
  // Replace with your search: a vector store, a database, or an internal API.
  return [{ title: 'Return an item', body: 'Start a return from Orders within 30 days of delivery.' }];
}

// AmplitudeToolLoop calls this once for each tool_use block in Claude's response.
export async function executeTool(name: string, input: Record<string, unknown>): Promise<string> {
  switch (name) {
    case 'get_order_status':
      return JSON.stringify(await getOrderStatus(String(input.order_id)));
    case 'search_help_docs':
      return JSON.stringify(await searchHelpDocs(String(input.query)));
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
```

**Why.** Return a string. What the executor returns becomes the `tool_result` content Claude reads next and the `[Agent] Tool Output` you see in Amplitude, so stringify objects yourself. When the executor throws, the loop sends Claude a `tool_result` with `is_error: true` and records the Tool Call with `[Agent] Tool Success` false, so let real failures throw instead of returning an error-shaped success.

`satisfies` checks the definitions against Anthropic's `Tool` type while keeping them plain objects, which is the shape the loop accepts.

## 4. Run the tool loop inside an agent session

`AmplitudeToolLoop` runs Anthropic's multi-turn `tool_use` loop: it calls Claude, runs every tool Claude asks for through your executor, sends the results back, and repeats until Claude answers. Open an agent session around it, keyed on an ID your app already has for the task, such as a ticket, thread, or run ID.

src/agent.ts

```typescript
// src/agent.ts
import type Anthropic from '@anthropic-ai/sdk';
import { AmplitudeToolLoop } from '@amplitude/ai';
import { ai, claude, MODEL, supportAgent } from './lib/amplitude.js';
import { executeTool, tools } from './tools.js';

const SYSTEM_PROMPT =
  'You are the support agent for an online store. Use the tools to look up orders and help articles. Answer in two or three sentences.';

export interface SupportTask {
  taskId: string; // a ticket, thread, or run ID your app already has
  prompt: string;
  userId?: string; // the same user ID your product events carry; omit when signed out
  deviceId?: string;
}

export async function runSupportAgent({ taskId, prompt, userId, deviceId }: SupportTask) {
  return supportAgent
    .session({ userId, deviceId, sessionId: taskId })
    .run(async () => {
      const loop = new AmplitudeToolLoop({ amplitudeAI: ai, userId, sessionId: taskId });

      const responses = await loop.run({
        client: claude,
        model: MODEL,
        system: SYSTEM_PROMPT,
        messages: [{ role: 'user', content: prompt }],
        tools,
        toolExecutor: executeTool,
      });

      // run() resolves with every raw Claude response; the last one holds the answer.
      const last = responses[responses.length - 1] as unknown as Anthropic.Message | undefined;
      if (!last || last.stop_reason === 'tool_use') {
        throw new Error(`Agent stopped before answering task ${taskId}`);
      }

      return last.content
        .filter((block): block is Anthropic.TextBlock => block.type === 'text')
        .map((block) => block.text)
        .join('');
    });
}
```

**Why.** The session gives the loop's events the agent ID and the device ID, and `run()` closes the session when the callback returns, so the task is scored without waiting for the 30-minute idle timeout. Don't call `s.trackUserMessage()` here: the loop records the `[Agent] User Message` from the messages you pass it, and a second call counts the question twice.

Pass only the messages for this task. The loop records a User Message for every user text message in the array on each `run()` call, so resending a long chat history records old questions again. Start a new task ID when the user starts a new job, rather than continuing the old session.

Use the same user ID your product events carry, and omit it when the user is signed out. Never send a placeholder like `"anonymous"`; Amplitude can't merge a placeholder into the real user later.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing taxonomy before it adds anything, so the product events your app already sends and the `[Agent]` events from this page end up in charts together instead of next to near-duplicates. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## 5. Keep cost right with prompt caching

Anthropic reports cached tokens separately: `input_tokens` excludes `cache_read_input_tokens` and `cache_creation_input_tokens`. Amplitude expects `inputTokens` to include them, with the cache counts passed alongside so each is priced at its own rate. The loop and Amplitude's Anthropic wrapper both add them up for you. If you call Claude yourself and track the response with `s.trackAiMessage()`, do the same arithmetic, or cache-heavy calls get priced wrong.

src/lib/claude-usage.ts

```typescript
// src/lib/claude-usage.ts
import type Anthropic from '@anthropic-ai/sdk';

// Only needed for Claude calls you track yourself with s.trackAiMessage().
export function claudeTokenCounts(usage: Anthropic.Usage) {
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
  const cacheCreationTokens = usage.cache_creation_input_tokens ?? 0;
  return {
    inputTokens: usage.input_tokens + cacheReadTokens + cacheCreationTokens,
    outputTokens: usage.output_tokens,
    cacheReadTokens,
    cacheCreationTokens,
  };
}
```

Use it as `s.trackAiMessage(text, MODEL, 'anthropic', latencyMs, claudeTokenCounts(response.usage))`. Do not add reasoning tokens separately; the deprecated `reasoningTokens` parameter is ignored.

If you want Claude's thinking recorded on `[Agent] AI Response`, make that call through the `Anthropic` wrapper from `@amplitude/ai`, which captures reasoning content along with streaming, tokens, and cost. The loop records the text of each response.

## 6. Flush before the process exits

The SDK batches events. Use `flushAgentEvents()` before a script exits or a serverless invocation returns. For a long-running server, call it after active requests finish during shutdown.

src/index.ts

```typescript
// src/index.ts
import { randomUUID } from 'node:crypto';
import { ai, flushAgentEvents } from './lib/amplitude.js';
import { runSupportAgent } from './agent.js';

const prompt = process.argv[2] ?? 'Where is order 1042, and can I still return it?';

try {
  const answer = await runSupportAgent({
    taskId: randomUUID(),
    prompt,
    userId: 'user-42', // use your signed-in user's ID
  });
  console.log(answer);
} finally {
  await flushAgentEvents();
}
```

For a long-running server, add `await flushAgentEvents()` to its existing shutdown sequence after it stops accepting new requests and drains active work.

**Why.** Without the flush, the script exits or the deploy stops the server with the last batch of agent events in memory, and nothing reports the loss. `flushAgentEvents()` waits for the underlying analytics upload attempt to finish. In a server, drain active requests before running the flush-and-exit handler.

## Verify it worked

Run the script with a question that needs both tools:

```sh
pnpm exec tsx --env-file=.env src/index.ts "Where is order 1042, and can I still return it?"
```

Then check in this order:

1. **Terminal.** With `debug` on outside production, every tracked event prints to stderr as a one-line summary, for example `[amplitude-ai] [Agent] AI Response | user=user-42 session=... agent=support-agent model=claude-sonnet-5-5 ...`. Expect a User Message, an AI Response that asks for tools, a Tool Call for each tool Claude used, and a final AI Response, followed by the answer on stdout. You can also run `pnpm exec amplitude-ai doctor`.
2. **Ingestion Debugger.** In Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart should show the script's uploads. It shows counts, not individual events.
3. **User lookup.** Go to Users > User Profiles and search `user-42`. Within ten seconds to a minute the event stream shows `[Agent] User Message`, `[Agent] AI Response`, and `[Agent] Tool Call`. Open an AI Response and confirm `[Agent] Model Name` (`claude-sonnet-5-5`), `[Agent] Provider` (`anthropic`), `[Agent] Input Tokens`, `[Agent] Output Tokens`, `[Agent] Cost USD`, `[Agent] Latency Ms`, `[Agent] Session ID` (the task ID), and `[Agent] Agent ID` (`support-agent`). Open a Tool Call and confirm `[Agent] Tool Name`, `[Agent] Tool Success`, `[Agent] Tool Input`, and `[Agent] Tool Output`.
4. **Agent analytics.** Open agent analytics in your project and find the session by its task ID. The `[Agent] Session Record` with quality signals typically arrives within 15 to 20 minutes after the session closes.

## Troubleshooting

### Every run fails with a 400 error about `max_tokens`

The loop got a client that doesn't set `max_tokens`. Pass `claude` from `src/lib/amplitude.ts`, not the bare Anthropic client.

### Each AI Response shows up twice

The loop got the `Anthropic` wrapper from `@amplitude/ai`, or `patch()` is on, so the wrapper and the loop both record every call. Give the loop the plain client from `@anthropic-ai/sdk`.

### User Messages repeat earlier questions

You passed the whole chat history to `loop.run()`. The loop records every user text message in the array, so pass the messages for the current task and give each new job its own task ID.

### `[Agent] Tool Output` reads `[object Object]`

The executor returned an object. Return `JSON.stringify(result)`.

### The run throws "Agent stopped before answering"

The loop stops after a fixed number of model calls, and Claude was still asking for tools. Check the Tool Calls in that session for a tool that keeps failing, and tighten its description so Claude stops retrying it.

### `[Agent] Cost USD` is empty

The model name isn't in the SDK's bundled price catalog, which happens with gateway aliases, fine-tuned models, and models newer than your SDK version. Pass the canonical model ID. For a new model, import `enableLivePriceUpdates` from `@amplitude/ai` and call it once at startup if outbound HTTPS to the public price catalog is allowed. A failed refresh falls back to the bundled catalog. `[Agent] Session Cost USD` is all-or-nothing, so one unpriced response leaves it off the Session Record.

### The last events never arrive

The process exited with events still buffered. Confirm `src/index.ts` awaits `flushAgentEvents()` in `finally`, and that your server drains active requests and awaits the same helper before exiting.

## FAQ

### Can I keep conversation text out of Amplitude?

Yes. Set the privacy mode to `metadata_only`. Tokens, cost, latency, models, and session grouping remain available; message text and tool payloads stay out. Built-in quality signals that need message text will not run.

### Should every message get a new agent session ID?

Use one stable ID for the same conversation or task, and a new ID for a new job. Never reuse an ID across users. These scope helpers close at the end of each request or task; later events with the same ID are stored but do not update the first Session Record.

### Why are events visible before quality signals?

Events normally appear within about a minute. The Session Record typically follows 15 to 20 minutes after an explicit close, and can take longer during an enrichment backlog.

## Full code reference

Every file this tutorial adds, in one block, the version to hand to an agent. `src/lib/claude-usage.ts` is only needed if you track Claude calls yourself.

Full code reference

```typescript
// -- .env --
// AMPLITUDE_AI_API_KEY=your-amplitude-project-api-key
// ANTHROPIC_API_KEY=your-anthropic-api-key

// -- src/lib/amplitude.ts --
import Anthropic from '@anthropic-ai/sdk';
import { AIConfig, AmplitudeAI } from '@amplitude/ai';

const apiKey = process.env.AMPLITUDE_AI_API_KEY;

if (!apiKey) {
  throw new Error('AMPLITUDE_AI_API_KEY is not set');
}

export const ai = new AmplitudeAI({
  apiKey,
  config: new AIConfig({
    contentMode: 'full',
    redactPii: true,
    debug: process.env.NODE_ENV !== 'production',
  }),
});

export const supportAgent = ai.agent('support-agent', {
  description: 'Answers order and help-center questions with tools',
});

// Wait for a promise-returning client or the analytics return object's promise.
export async function flushAgentEvents(): Promise<void> {
  const pending = ai.flush();
  if (pending && typeof pending === 'object' && 'promise' in pending) {
    await pending.promise;
  } else {
    await pending;
  }

export const MODEL = 'claude-sonnet-5-5';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

type LoopRequest = Omit<Anthropic.MessageCreateParamsNonStreaming, 'max_tokens'> & {
  max_tokens?: number;
};

export const claude = {
  messages: {
    create: (params: LoopRequest) =>
      anthropic.messages.create({ ...params, max_tokens: params.max_tokens ?? 4096 }),
  },
};

// -- src/tools.ts --
import type Anthropic from '@anthropic-ai/sdk';

export const tools = [
  {
    name: 'get_order_status',
    description:
      'Looks up the shipping status of one order by its order number. Use it when the user asks where an order is, when it ships, or whether it arrived. Returns the status, carrier, and estimated delivery date. It does not return payment or refund details.',
    input_schema: {
      type: 'object',
      properties: {
        order_id: { type: 'string', description: 'The order number, for example 1042.' },
      },
      required: ['order_id'],
    },
  {
    name: 'search_help_docs',
    description:
      'Searches the help center for articles about returns, refunds, shipping, and account settings. Use it for policy questions the order record cannot answer. Returns up to three matching articles with a title and body.',
    input_schema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'A short search phrase, for example "return window".' },
      },
      required: ['query'],
    },
] satisfies Anthropic.Tool[];

async function getOrderStatus(orderId: string) {
  return { order_id: orderId, status: 'shipped', carrier: 'UPS', estimated_delivery: '2026-10-18' };
}

async function searchHelpDocs(query: string) {
  return [{ title: 'Return an item', body: 'Start a return from Orders within 30 days of delivery.' }];
}

export async function executeTool(name: string, input: Record<string, unknown>): Promise<string> {
  switch (name) {
    case 'get_order_status':
      return JSON.stringify(await getOrderStatus(String(input.order_id)));
    case 'search_help_docs':
      return JSON.stringify(await searchHelpDocs(String(input.query)));
    default:
      throw new Error(`Unknown tool: ${name}`);
  }

// -- src/agent.ts --
import type Anthropic from '@anthropic-ai/sdk';
import { AmplitudeToolLoop } from '@amplitude/ai';
import { ai, claude, MODEL, supportAgent } from './lib/amplitude.js';
import { executeTool, tools } from './tools.js';

const SYSTEM_PROMPT =
  'You are the support agent for an online store. Use the tools to look up orders and help articles. Answer in two or three sentences.';

export interface SupportTask {
  taskId: string;
  prompt: string;
  userId?: string;
  deviceId?: string;
}

export async function runSupportAgent({ taskId, prompt, userId, deviceId }: SupportTask) {
  return supportAgent
    .session({ userId, deviceId, sessionId: taskId })
    .run(async () => {
      const loop = new AmplitudeToolLoop({ amplitudeAI: ai, userId, sessionId: taskId });

      const responses = await loop.run({
        client: claude,
        model: MODEL,
        system: SYSTEM_PROMPT,
        messages: [{ role: 'user', content: prompt }],
        tools,
        toolExecutor: executeTool,
      });

      const last = responses[responses.length - 1] as unknown as Anthropic.Message | undefined;
      if (!last || last.stop_reason === 'tool_use') {
        throw new Error(`Agent stopped before answering task ${taskId}`);
      }

      return last.content
        .filter((block): block is Anthropic.TextBlock => block.type === 'text')
        .map((block) => block.text)
        .join('');
    });
}

// -- src/lib/claude-usage.ts (optional) --
import type Anthropic from '@anthropic-ai/sdk';

export function claudeTokenCounts(usage: Anthropic.Usage) {
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
  const cacheCreationTokens = usage.cache_creation_input_tokens ?? 0;
  return {
    inputTokens: usage.input_tokens + cacheReadTokens + cacheCreationTokens,
    outputTokens: usage.output_tokens,
    cacheReadTokens,
    cacheCreationTokens,
  };
}

// -- src/index.ts --
import { randomUUID } from 'node:crypto';
import { ai, flushAgentEvents } from './lib/amplitude.js';
import { runSupportAgent } from './agent.js';

const prompt = process.argv[2] ?? 'Where is order 1042, and can I still return it?';

try {
  const answer = await runSupportAgent({
    taskId: randomUUID(),
    prompt,
    userId: 'user-42',
  });
  console.log(answer);
} finally {
  await flushAgentEvents();
}
```

## Hand it to your agent

Prefer to have Cursor, Claude Code, or Codex CLI do the wiring? This prompt points the agent at this page and pins the decisions that matter.

Using Cursor or Claude Code? Paste this prompt

```text
Add Amplitude agent analytics to this Claude tool-use agent by following
https://amplitude.com/build/anthropic-agent-analytics exactly. Install
@amplitude/ai, @amplitude/analytics-node, and @anthropic-ai/sdk. Create
src/lib/amplitude.ts that constructs AmplitudeAI from
process.env.AMPLITUDE_AI_API_KEY, the agent with ai.agent(), and a plain
Anthropic client from @anthropic-ai/sdk, all at module scope; never call
ai.agent() per request. Give AmplitudeToolLoop a client adapter whose
messages.create sets max_tokens, and never the Anthropic wrapper from
@amplitude/ai. Open agent.session({ userId, deviceId, sessionId }) with
the task's existing ID as sessionId and run loop.run() inside run().
Pass only the current task's messages, don't call trackUserMessage
yourself, and make the tool executor return strings and throw on
failure. Use the canonical model ID. Await flushAgentEvents() before a script
exits; in a server drain active requests, await flushAgentEvents(), then exit. Never pass a placeholder
userId. Don't hardcode keys. Report every file you changed.
```

## What this unlocks

- Chart `[Agent] Cost USD` and `[Agent] Latency Ms` by model on AI Response events.
- Connect agent and product events in funnels when they use the same stable user ID.
- Review content-based quality signals on the Session Record after the session closes, when your privacy mode sends message text.

## Next steps

[Web frameworksHow to add agent analytics to a Next.js appRoute handlers, the Node runtime, and streaming on Vercel.](https://amplitude.com/build/nextjs-agent-analytics)[Use caseHow to monitor agent tool callsChart tool failure rates and latency from the \`\[Agent\] Tool Call\` events this page sends.]()[ReferenceTrack a Claude Agent SDK agentBuilding on the Claude Agent SDK instead of the Messages API? \`ClaudeAgentSDKTracker\` hooks into its tool calls and message stream.](https://amplitude.com/docs/sdks/agent-analytics/sdk#claude-agent-sdk)

Reference docs

[Agent analytics quickstart →](https://amplitude.com/docs/amplitude-ai/agent-analytics/quickstart)[Amplitude AI SDK reference →](https://amplitude.com/docs/sdks/agent-analytics/sdk)[Agent analytics sessions and user identity →](https://amplitude.com/docs/amplitude-ai/agent-analytics/sessions)[Agent analytics taxonomy →](https://amplitude.com/docs/amplitude-ai/agent-analytics/taxonomy)[Verify your agent analytics instrumentation →](https://amplitude.com/docs/amplitude-ai/agent-analytics/verify)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

For agents

[View raw .md](/build/anthropic-agent-analytics.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

Use the verification steps above to check your agent events, then configure the reports and replay links you need.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
