On this page

Set up Agent Analytics

This page describes how to customize your Agent Analytics set up. To complete the in-depth set up, you need to modify your environment with the Agent Analytics SDK. The Agent Analytics SDK is the full developer reference, covering install, initialization, instrumenting sessions and tools, provider notes, edge runtimes, in-process OTel span ingestion, cost handling, and the full API. If you'd rather send OpenTelemetry traces from a Collector than instrument with the SDK, refer to Send OpenTelemetry traces directly. If you want to quickly get started with basic Agent Analytics, go to the Agent Analytics Quickstart Guide. The quickstart guide doesn't allow customizations. You can always complete the quickstart guide to get insights as fast as possible and then come back to this set up guide to customize your Agent Analytics insights.

The timeline below shows what your instrumentation produces. Click any event to inspect its shape.

From the SDKreal-time during sessionFrom Amplitudepost-hoc enrichmentLOADTURN 1TURN 2ENDViewedPage(browser SDK)User MessageTool CallAI ResponseUser MessageTool CallAI Response···Session EndALSO EMITTED — INSIDE A TURNSpanSession RecordEvaluator Result × N
Event type
[Agent] AI Responsefrom the SDK
Fired at
22:33:48
Identity
[Agent] Session ID4ddcc6b2-1041-432a-aa8c-ebe3eccac40b
[Agent] Agent IDsupport-chatbot
[Agent] Trace IDb4f63d43-d752-4b1f-8489-d234ddf586b2
Event-specific
$llm_message.textI can help. Your subscription renews on Aug 15…
[Agent] Model Namegpt-4o-mini
[Agent] Provideropenai
[Agent] Input Tokens1245
[Agent] Output Tokens87
[Agent] Latency Ms3420
[Agent] Cost USD0.0012
Closes the turn. Carries the eight fields the SDK doctor checks at setup: Session ID, Agent ID, Model, Provider, Latency Ms, Input/Output Tokens, Cost USD. Emitted by s.trackAiMessage(...) or a provider wrapper.

How setup works

You must have previously completed the following prerequisites before data shows up in Amplitude:

  1. You've selected which Amplitude project the data will flow into and you have access to the corresponding project API Key.
  2. You've instrumented your code. The Amplitude AI SDK (Node and Python) covers most stacks. Runtimes the SDK doesn't bundle into can send events directly to the HTTP API. These runtimes include Cloudflare Workers, other edges, and unsupported languages such as Java, Go, and Ruby.
  3. You've decided what leaves your app. Privacy mode controls whether prompt and response text reaches Amplitude.

Choose an instrumentation path

Whatever the path, the goal is the same: agents plus sessions plus wrapped providers. That combination is what unlocks per-user analytics, enrichment, and quality measurement.

Define your sessions

An agent session is one unit of work with a start, a set of turns, and an outcome, such as a ticket resolved, a task completed, or a conversation ended. It groups the events that produced that outcome (User Message, AI Response, Tool Call, Score, Span) under one [Agent] Session ID. A session opens when your code first attaches events to that ID, stays open while activity keeps arriving, and closes either explicitly or by idle timeout. Enrichment (Session Record, signals, evaluator results) runs once per session, after it closes.

Every session belongs to a user; pass the same user ID you use for product analytics. Refer to Setting user IDs for identity rules.

Choose a session ID

Pass the ID your application already has for "this unit of work."

If nothing fits, generate a stable UUID at session-start and persist it wherever your app already keeps unit-of-work state (ticket row, thread record, request context). Threading the same ID through every request handler is what keeps turns in one session; don't generate a fresh ID per request.

New goal, new session. Reusing a session ID across unrelated users conflates their data.

How sessions close

This is the most important lifecycle concept in Agent Analytics, because enrichment only runs on closed sessions. If your [Agent] Session Record events aren't appearing, start here.

A session closes one of two ways.

  1. Explicit close (recommended): Call trackSessionEnd() (Node) or track_session_end() (Python) when the unit of work finishes: ticket resolved, call ended, run completed. Sessions opened with a scope helper close automatically when that scope exits: run() in Node, the session() context manager in Python. The close is written when the next ingestion batch processes the Session End event, so in the typical case expect the [Agent] Session Record event within about 15 to 20 minutes; it can take longer while an org has an enrichment backlog.

    • Closing without the AI SDK: The server closes a session on the [Agent] Session End event itself, so you can send that event directly from any Amplitude SDK or the HTTP API. Set the event type to [Agent] Session End, include an [Agent] Session ID property matching the session you're closing, and send it to the project where you've enabled Agent Analytics.

      json
      {
        "event_type": "[Agent] Session End",
        "user_id": "user-123",
        "event_properties": {
          "[Agent] Session ID": "session-abc"
        }
      }
      

      This closes the session exactly as an SDK-emitted close does. Amplitude stores any additional properties you include with the event, but they don't affect closing.

  2. Idle timeout (automatic fallback): If you never close explicitly, the server closes the session after 30 minutes of inactivity by default, measured from the last agent event received for the session. Any of these six [Agent] events resets the clock: User Message, AI Response, Tool Call, Session End, Score, and Span. Server-generated enrichment events, including Session Record and Evaluator Result, do not. Ingestion runs on a cycle, so the close lands within roughly 15 minutes after the idle window elapses.

What closing does and doesn't do. Closing marks completion; it doesn't lock the session. Amplitude still accepts and stores later events, but enrichment runs once, so late turns never reach the session's signals, rollups, or Session Record. Only the first close counts: Amplitude ignores duplicates, and also ignores a [Agent] Session End event that arrives after the idle timeout already closed the session. The recommendation is to close when the job is genuinely done; if real work continues, start a new session.

Choosing a timeout

On top of the idle window, sessions without a per-session override also close after 24 hours regardless of activity. Setting any override (a positive value or -1) exempts the session from that cap.

Set the timeout too low and a long pause closes the session early: follow-up events still attach and Amplitude stores them, but they land after enrichment has run, so they never reach the Session Record. Reach for -1 when quiet stretches are normal and unbounded, such as a ticket idle over a weekend, a long-running background job, and any fixed timeout would eventually split a real job. The tradeoff is that a session your app forgets to close stays open and unenriched until the 90-day backstop closes it.

Two caveats on -1:

  • Agents with enrichment disabled close on the org default schedule
  • The 90-day limit counts from when the session started, so a session that stays active for more than 90 days straight never auto-closes at all.

Where to set the override

Set idleTimeoutMinutes (Node) or idle_timeout_minutes (Python) when you open the session, and also include an idle_timeout_minutes key in the agent's context. Today the context route is what reliably reaches the server for sessions that never close explicitly; setting both guarantees the override takes effect and keeps working after the server-side fix ships.

typescript
// Node
const agent = ai.agent('support-bot', {
  context: { idle_timeout_minutes: 240 },   // reliable today
});
const session = agent.session({
  userId,
  sessionId: ticketId,
  idleTimeoutMinutes: 240,                  // the intended parameter
});

Checking which path ran

Every Session Record carries [Agent] Close Reason, either explicit_close or timeout, so you can monitor how often the fallback is doing the work.

Long-running sessions

The SDK batches events in memory and ships them on an interval. For short sessions (under a minute), batching is transparent. For long-running sessions such as tickets worked over hours, coding tasks that span a day, or background jobs, the close event you emit at the end can sit in the buffer, and a process restart before it ships means the session stays open until the idle timeout catches it.

Call ai.flush() right after trackSessionEnd() (Node) or track_session_end() (Python) to guarantee the close lands before the process exits or restarts:

typescript
agent.trackSessionEnd({ sessionId: ticketId });
await ai.flush();

Serverless handlers auto-flush on session.run() completion, so you don't need an explicit call there. For long-running servers, also call ai.flush() in your SIGTERM handler so buffered events across every open session ship before the process dies.

Choose a privacy mode

Three content modes control what leaves your infrastructure. Set the mode once in SDK config.

Amplitude-generated enrichments (signals, topics, custom evaluators) run in every mode. What differs is what those enrichments see: in modes without content, expect content-dependent signals such as task completion and response quality to carry lower information, while structural signals (errors, friction patterns from behavior, cost, latency) remain useful.

Regulated environments (healthcare, finance) typically run metadata_only or customer_enriched. Decide the mode before wide rollout; it determines what your legal review needs to cover.

full

Message content, system prompts, tool inputs and outputs, and score comments all reach Amplitude. Amplitude's built-in enrichment runs and produces the fullest signal set.

  • System prompt is an optional field; you can omit it.
  • PII redaction defaults to on and scrubs emails, phone numbers, SSNs, credit cards, and IP addresses before events leave your process. Built-in phone and SSN detection targets US formats; add custom regex patterns or plug in your own redaction (for example, Presidio) for international locales or domain-specific identifiers.

metadata_only

No message content, system prompts, tool payloads, or score comments leave your process. You still get tokens, cost, latency, model names, and session grouping, so analyses that don't require content still work. Content-dependent signals carry lower information; structural signals remain useful.

customer_enriched

No content leaves your process, but Amplitude enrichment runs on the labels you provide via trackSessionEnrichment(). Your enrichment is the only session-level quality data, because Amplitude's signals have no content to work with.

  • customer_enriched isn't exclusive. You can send Session Enrichment events in any mode; this mode is simply the one where those events are the sole source of session-level quality data.

Setting user IDs

Amplitude counts unique users by combining three identifiers (device ID, user ID, and Amplitude ID) into a single profile across anonymous sessions, sign-ins, and multiple devices. Send a stable user ID as soon as a person authenticates so anonymous events on the same device merge into one profile and your active-user counts stay accurate.

Every [Agent] event carries your stable user ID and unlocks cross-domain funnels, cohorts, and retention chart analyses.

If Amplitude encounters a known device ID that is already tied to a user ID in a different project, Amplitude assumes the device ID is tied to that user ID in all projects, even if you don't have the Portfolio add-on. For more information, refer to Portfolios.

Two rules keep a single user from splitting into two:

  • Never pass a placeholder user ID such as "anonymous", an empty string, or a temporary ID. A user ID can't be changed once set, so a placeholder creates a permanent separate user that won't merge later. Omit the userId instead; anonymous activity merges into the known identity through standard identity resolution once the user identifies.
  • Reuse the same device ID across a pre-account session. If your backend generates a new device ID per request, the user identity merge breaks. If possible, read the device ID from the Browser SDK and forward it.

Decide your ID strategy on day one of instrumentation.

Agentic SDK

Available for Node and Python only.

plaintext
Instrument this app with Amplitude Agent Analytics using the Node SDK.

Install the SDK:

npm install @amplitude/ai @amplitude/analytics-node

Then follow `node_modules/@amplitude/ai/amplitude-ai.md`.

Paste this prompt into your AI coding agent (Cursor, Claude Code, Windsurf, GitHub Copilot, or Codex). The agent reads the linked instructions file, scans your codebase, finds every LLM call site and the session lifecycle, and instruments them.

Manual SDK

Available for Node and Python only.

Wrap your LLM client, name an agent, and run each conversation inside a session. That pattern produces every event type the product expects.

typescript
import { AmplitudeAI, OpenAI } from '@amplitude/ai';

const ai = new AmplitudeAI({ apiKey: process.env.AMPLITUDE_AI_API_KEY });
const openai = new OpenAI({ amplitude: ai, apiKey: process.env.OPENAI_API_KEY });
const agent = ai.agent('my-agent');

const session = agent.session({ userId, sessionId });
await session.run(async (s) => {
  s.trackUserMessage(message);
  return openai.chat.completions.create({ model, messages });
});
await ai.flush();

The wrapped provider (new OpenAI({ amplitude: ai }) in Node, patch(amplitude_ai=ai) in Python) auto-captures [Agent] AI Response, including the chat text, from the completion response. Without a wrapped provider, Amplitude doesn't track that event or its message content. If you're using a provider the SDK doesn't wrap, or you're calling an unwrapped client directly, call trackAiMessage() (Node) or track_ai_message() (Python) after the completion returns. For details and all options, refer to Manual instrumentation in the SDK reference.

Send OpenTelemetry traces directly

Limited availability

Amplitude enables the OTLP endpoint per project. Contact your Amplitude account team to enable it. Until then, the endpoint returns 403.

If your stack already emits OpenTelemetry GenAI spans, point your exporter at Amplitude. No Amplitude SDK, no re-instrumentation. Amplitude translates gen_ai.* spans into the same [Agent] events the SDK produces, so sessions, enrichment, and every chart work identically.

This is a different path from the in-process OTel exporters, which run inside a Node or Python app that already uses the AI SDK.

Configure your exporter

bash
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.amplitude.com/otlp/v1/traces   # US
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.eu.amplitude.com/otlp/v1/traces  # EU
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <project API key>"
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf   # or http/json
  • OTLP over HTTP only. http/protobuf (the OpenTelemetry SDK and Collector default) and http/json are both accepted, and gzip is supported. OTLP/gRPC isn't supported: bridge through a local Collector with an OTLP/HTTP exporter.
  • Amplitude accepts the project API key as a Bearer token or as an api_key query parameter.
  • Instrumentation Amplitude understands: OpenTelemetry GenAI semantic conventions, OpenInference (llm.*), OpenLLMetry and Traceloop (traceloop.*), OpenLIT, and LiteLLM. Amplitude ignores non-GenAI spans in the same pipeline, so a mixed Collector is safe to point at Amplitude.

Set session and user attributes

This is the one thing worth changing in your instrumentation. Everything else maps automatically.

An Amplitude agent session is a whole conversation, not one trace. Set a conversation attribute, or every trace becomes its own single-turn session: task completion and abandonment then read as noise, and, since billing is per agent session, one conversation bills as several.

What maps to what

To report costs for directly exported spans, set gen_ai.usage.cost. Amplitude doesn't calculate a cost when this attribute is absent. Refer to How cost is calculated for details.

Because a chat call re-sends the whole prompt history, only the user messages that follow the last assistant message become a new [Agent] User Message. Amplitude doesn't re-emit replayed history, and a tool-loop continuation whose new input is a tool result produces no spurious user turn.

Content and privacy

Most OpenTelemetry GenAI instrumentation treats message content as opt-in. Leave content capture off and you get the metadata_only shape: cost, tokens, latency, model, and sessions, with no transcripts. Turn it on and gen_ai.input.messages, gen_ai.output.messages, and gen_ai.system_instructions flow into $llm_message and [Agent] System Prompt, where Amplitude's redaction and enrichment apply. Redaction that has to happen before data leaves your network belongs in a Collector processor.

Delivery, limits, and responses

Delivery is at least once. Run a Collector with a persistent sending queue, since an in-memory queue loses buffered spans on restart, and keep retries enabled.

Verify

Send one traced conversation, then confirm the events arrive with a real [Agent] Session ID rather than a trace ID, a populated [Agent] Agent ID, and your own user IDs rather than unknown. Those three are what the attribute chains above buy you.

Send agent events without the AI SDK

The AI SDK is the lowest-friction path, but it isn't required, and it's Node.js and Python only. If your stack can't run it, emit the [Agent] taxonomy directly with a standard Amplitude SDK or the HTTP API. Common cases: browsers and client-heavy SPAs, edge runtimes (Cloudflare Workers, Deno), and AI app builders such as Lovable, Superblocks, v0, or Bolt, where the generated app is browser-first and importing @amplitude/ai fails on Node-only modules.

This section is the canonical wire contract for that path. Without the SDK, three responsibilities shift to you: session lifecycle (you emit Session End), privacy (you redact before tracking), and ID discipline (you generate and thread the IDs below).

The event contract

Emit these events with track() from any standard Amplitude SDK, or using the HTTP API. The wire format is a POST to Amplitude's HTTP endpoint carrying the [Agent] event contract as JSON:

bash
curl -X POST https://api2.amplitude.com/2/httpapi \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "YOUR_API_KEY",
    "events": [{
      "event_type": "[Agent] User Message",
      "user_id": "user-123",
      "event_properties": {
        "[Agent] Session ID": "ticket-42",
        "[Agent] Turn ID": 1,
        "[Agent] Trace ID": "trace-abc",
        "[Agent] Agent ID": "support-bot",
        "[Agent] Message ID": "msg-1",
        "$llm_message": { "text": "I need a refund" }
      }
    }]
  }'

For EU data residency, replace the endpoint with https://api.eu.amplitude.com/2/httpapi. Everything below applies to both the standard-SDK track() route and this raw HTTP route.

Minimum viable:

  • [Agent] User Message
  • [Agent] AI Response
  • [Agent] Session End
  • Optional: add [Agent] Tool Call when your agent uses tools.

Every event carries the shared event properties: [Agent] Session ID, [Agent] Turn ID, [Agent] Trace ID, [Agent] Agent ID, plus [Agent] Env, [Agent] Runtime (for example browser), and optionally [Agent] Context. Send the same user ID you use for product analytics. Always set [Agent] Agent ID. Without it, the Agent Analytics consumer still accepts the event but can't correlate it into a session, so it lands misgrouped and stays out of Agent Analytics views. The HTTP API still returns 200 because it acknowledges receipt before the consumer runs, so don't rely on that response to confirm the event landed correctly. Stamp a stable agent ID on every event.

Rules that hand-rolled instrumentation gets wrong most often:

  • $llm_message must be an object: { text: "..." }. Amplitude silently ignores a plain string, and the thread view renders no message content.
  • Turn ID identifies the exchange, not the event. Increment it once per user-message round trip and stamp the same value on the user message, every tool call, and the AI response of that exchange. Ordering within a turn comes from event time, so emit tool calls before the AI Response in real execution order. Generate one [Agent] Trace ID per round trip and share it across the same events.
  • Stay inside the taxonomy. Don't invent event types or properties under the [Agent] prefix; unregistered ones may not be queryable in charts. For business actions the agent performs (a purchase, a booking, a recommendation click), emit your standard product events with your existing names and properties, carrying the same user_id; that keeps agent-driven and click-driven journeys comparable in one funnel.
  • Provide cost for direct events. Amplitude doesn't calculate [Agent] Cost USD from model and token properties, so include it when you want cost reporting. Refer to How cost is calculated for details.
  • Token and cost properties live only on AI Response. Tool calls don't consume tokens; the deciding call and the follow-up call are both part of the AI Response. Don't place [Agent] Cost USD on other event types: the server sums that property from any event it ingests, so a stray cost on a Tool Call or Session End silently inflates the session total.

Session lifecycle without the SDK

The server closes any idle session after 30 minutes automatically, so you have a safety net even if you never emit Session End. Emit [Agent] Session End when the job genuinely ends (chat closed, ticket resolved, run completed), and let the server timeout catch abandonment.

Setting the idle override without the SDK: include an idle_timeout_minutes key inside the [Agent] Context JSON on your events (any event works; first value wins), and mirror it as [Agent] Session Idle Timeout Minutes on your Session End if you send one. The Context route is what the server reliably reads today. For example, on the first user message of the session:

javascript
amplitude.track('[Agent] User Message', {
  '[Agent] Session ID': ticketId,
  '[Agent] Turn ID': 1,
  '[Agent] Trace ID': traceId,
  '[Agent] Agent ID': 'support-bot',
  '[Agent] Message ID': messageId,
  '[Agent] Context': JSON.stringify({ idle_timeout_minutes: 240 }),
  $llm_message: { text: redactedText },
});

// ...and if you emit an explicit close when the ticket resolves:
amplitude.track('[Agent] Session End', {
  '[Agent] Session ID': ticketId,
  '[Agent] Agent ID': 'support-bot',
  '[Agent] Session Idle Timeout Minutes': 240,
});

Don't build your own short idle timer. Rotating session IDs after a few quiet minutes chops one conversation into several sessions: enrichment judges half-conversations, task completion looks artificially low, and, since billing is per agent session, one conversation bills as several. If your product genuinely defines sessions by inactivity, pass a matching [Agent] Session Idle Timeout Minutes and keep the window generous. Refer to the FAQ for the two-clocks explanation.

Privacy without the SDK

You own redaction, and it must run before track(). The most common mistake is gating message content while forgetting the other content-bearing properties. Gate all four consistently with your chosen posture:

  • [ ] $llm_message.text on User Message and AI Response
  • [ ] [Agent] System Prompt on AI Response
  • [ ] [Agent] Tool Input and [Agent] Tool Output on Tool Call (redact recursively; payloads are objects)
  • [ ] [Agent] Comment on Score

For metadata_only behavior, omit all four entirely; you still get cost, latency, tokens, and session grouping. For full behavior, redact PII (emails, phones, SSNs, cards, IPs) before tracking.

Browser and edge notes

  • Identity: a persistent anonymous UUID in localStorage as user_id gives anonymous visitors stable identity; standard identity resolution merges them when they sign in.
  • Session Replay: with the replay plugin active, stamp the replay properties onto every agent event so sessions link to recordings.
  • Do not set the beacon transport globally. Calling setTransport('beacon'), for example inside a session-end handler, applies permanently to the whole SDK: every later event fires fire-and-forget with no retry and drops silently. Use the default transport.
  • Latency and tokens are best measured server-side and returned to the client alongside the response, then attached to the events.

Recipe: instrument inside an AI app builder

In Lovable, Superblocks, or any prompt-driven builder, paste this into the builder's agent and review what it produces:

plaintext
Instrument this app's chat agent with Amplitude Agent Analytics using the standard
@amplitude/analytics-browser SDK (do NOT use @amplitude/ai, it is Node-only and will
crash this runtime). Emit these events via amplitude.track(): [Agent] User Message at
request start, one [Agent] Tool Call per tool invocation in execution order,
[Agent] AI Response after the reply, and [Agent] Session End when the chat genuinely ends
(no short idle timers; the server auto-closes idle sessions). On every event include
[Agent] Session ID (one stable UUID per conversation), [Agent] Turn ID (increment once
per user-message exchange and stamp the same value on all events of that exchange),
[Agent] Trace ID (new UUID per exchange), [Agent] Agent ID (a stable name for this agent),
and [Agent] Runtime: "browser". Message text goes in $llm_message as { text: "..." }
(an object, not a string) on User Message and AI Response only. Put [Agent] Model Name,
[Agent] Provider, token counts, [Agent] Latency Ms, and [Agent] System Prompt on AI
Response only. Put [Agent] Tool Name, [Agent] Tool Success, [Agent] Latency Ms,
[Agent] Invocation ID, and [Agent] Parent Message ID on Tool Call. Redact PII (emails,
phones, SSNs, cards, IPs) from all message text, system prompts, and tool payloads
before tracking. Use one persistent anonymous UUID from localStorage as the Amplitude
user_id. Never call setTransport("beacon"). Use functional state updates when computing
turn counters from UI state so follow-up messages aren't lost to stale closures.

Then verify: send two messages, one that triggers a tool, and confirm in Live Events that all events share a session ID, each exchange shares one Turn ID and Trace ID, tool calls precede their AI Response, and $llm_message.text renders in the session thread view.

Instrument thumbs up / thumbs down feedback

Wire your feedback control to the SDK's scoring method so every rating lands as an [Agent] Score event. Three decisions matter:

  1. Name it user-feedback. This exact Score Name has special semantics: your explicit feedback overrides the detected negative feedback signal for the session, so a thumbs up from the user beats a false-positive frustration detection, and a thumbs down flags the session even when the model saw nothing wrong.
  2. Target the message. Rate the specific AI response the user reacted to by passing its [Agent] Message ID as the target, with target type message. Use target type session only for end-of-conversation ratings like a CSAT prompt.
  3. Value 1 for up, 0 for down, source user.
typescript
// Node: inside the session, when the user clicks the control
s.score('user-feedback', 1, aiMessageId, { source: 'user' });          // thumbs up
s.score('user-feedback', 0, aiMessageId, {
  source: 'user',
  comment: freeTextFromUser,   // optional; gated by privacy mode
});                                                                      // thumbs down

Feedback often arrives after the request that produced the response has finished (the user clicks minutes later, from the frontend). For that path, call the client-level method with the session and target IDs you stored alongside the message:

typescript
ai.trackScore({
  userId,
  name: 'user-feedback',
  value: 0,
  targetId: aiMessageId,
  targetType: 'message',
  source: 'user',
  sessionId,
});

Notes:

  • Ratings on a five-star or CSAT scale work the same way: pass the numeric value and, if it isn't binary thumbs, use a different Score Name (for example csat) so your thumbs metric stays clean. Reserve user-feedback for the binary control because of its override semantics.
  • Don't worry about sparse clicks. Built-in signals score every session regardless, and implicit behavior (copy, regenerate, abandonment) is captured automatically; explicit ratings sharpen the picture where you have them.
  • Amplitude never generates Score events, so a chart of [Agent] Score where Score Name is user-feedback is a pure measure of what your users said. The classic quality chart is the disagreement view: sessions the signals judged fine that users thumbed down.

Over HTTP without the SDK, POST the same event shape and the override semantics apply identically:

bash
curl -X POST https://api2.amplitude.com/2/httpapi \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "YOUR_API_KEY",
    "events": [{
      "event_type": "[Agent] Score",
      "user_id": "user-123",
      "event_properties": {
        "[Agent] Session ID": "ticket-42",
        "[Agent] Agent ID": "support-bot",
        "[Agent] Score Name": "user-feedback",
        "[Agent] Score Value": 1,
        "[Agent] Target ID": "msg-1",
        "[Agent] Target Type": "message",
        "[Agent] Evaluation Source": "user"
      }
    }]
  }'

The agent sessionId and the browser session ID identify different things. The agent session is the job (a ticket, a conversation) and your application owns that ID. The browser session is the visit, and Amplitude's Session Replay generates that ID on the frontend.

If your frontend runs Amplitude's Session Replay, pass browserSessionId and deviceId (from the standard Amplitude Analytics Browser SDK) when you open the agent session on the backend. The Session Replay ID (<deviceId>/<sessionId>) then lands as [Amplitude] Session Replay ID on the session's [Agent] events. Once at least one event carries it, a session replay link shows in the sessions pane.

Where each ID comes from:

  • Agent sessionId: your application generates or reuses this from your existing unit-of-work ID (thread, ticket, task). Refer to Choose a session ID.
  • browserSessionId and deviceId: the Amplitude Analytics Browser SDK maintains both on the client. Read them with amplitude.getSessionId() and amplitude.getDeviceId(), then forward them to your backend on the request that opens the agent session.

Multi-agent systems

Create child agents from a parent to record delegation. Child agents automatically carry the parent reference, sessions record the root agent and chain depth, and provider wrappers suppress spurious user-message events inside delegated calls. Give every agent a stable, human-readable ID; those IDs become the primary dimension for comparing quality and cost across your system.

Declare children off the parent with .child(), then dispatch to them with runAs() (Node) or arun_as() (Python). All events stay correlated under one [Agent] Session ID.

typescript
const orchestrator = ai.agent('shopping-agent', { description: 'Orchestrates shopping requests' });
const recipeAgent = orchestrator.child('recipe-agent', { description: 'Finds recipes' });

await orchestrator.session({ userId }).run(async (s) => {
  s.trackUserMessage(userInput);
  const result = await s.runAs(recipeAgent, async (cs) => {
    cs.trackUserMessage(delegatedQuery);
    return openai.chat.completions.create({ model: 'gpt-4o', messages: [...] });
  });
});

For the full delegation semantics (context inheritance, span wrapping, nested children), refer to the SDK reference's Multi-agent architectures section.

Verify your instrumentation

Verification checks eight gates on your events:

  1. Events arriving at all
  2. Model Name and Provider populated
  3. Token counts populated
  4. Cost USD populated
  5. Latency populated
  6. Session ID is your real ID, not an auto-generated placeholder
  7. Agent ID set per component
  8. User ID from your auth layer

The setup agent writes a verification test as part of instrumentation. If you instrumented by hand, run one real interaction and check the gates in Live Events. Gates 6 through 8 intentionally fail on placeholder IDs; that's the ladder, not a bug.

Long-lived servers and multi-turn HTTP

Three placement rules keep sessions grouped correctly on an HTTP server that handles many turns and users.

Define agents at module scope, not per request. Constructing ai.agent(...) inside a request handler mints a fresh [Agent] Agent ID on every turn and breaks session grouping. Create the agent once at module load and reuse the reference.

typescript
// agent.ts (module scope)
export const agent = ai.agent('support-bot');

// handler.ts
export async function POST(req: Request) {
  const { sessionId, userId, message } = await req.json();
  return agent.session({ userId, sessionId }).run(async (s) => {
    s.trackUserMessage(message);
    // ...
  });
}

Open one session per request, threading a stable sessionId. Call agent.session({ sessionId }) inside each request handler with the same sessionId value across every turn of the same job. Turns stitch together because they share the ID, not because they share the process.

Never reuse a sessionId across users. The session belongs to one user's job; a shared or global session ID conflates their conversations and breaks per-user analytics. If you emit [Agent] events over the HTTP API instead of the SDK, the same rule applies to the raw [Agent] Session ID property.

Production checklist

Once your traces start flowing into Amplitude, validate the following:

  • Valid user IDs are being used
  • Valid session IDs are being used
  • There are explicit session close events where the outcome is known, and there are idle timeouts when expected
  • Privacy mode decided and reviewed
  • Agent IDs are stable and human-readable; child agents are wired for delegation

Running custom evaluators

Amplitude's built-in signals run without any setup on your part. But if you want to run custom-defined evaluators built for your own quality criteria, or calibration runs that compare an evaluator's judgments against your own labeled examples to tune its accuracy, you need to bring your own model provider key (BYOK). Add a key under Settings > AI Controls > Model Providers for any of the supported providers:

  • OpenAI
  • Azure OpenAI
  • Anthropic
  • Google Gemini
  • Fireworks AI
  • Amazon Bedrock
  • Google Vertex AI

Custom evals and calibration runs use this key to make model calls on your behalf. Amplitude encrypts keys at rest and doesn't show them again after you save them, so keep your own copy if you need to reference or rotate it later.

Was this helpful?