Claude Agent SDK

Trace the Claude Agent SDK with Neatlogs (TypeScript).

Import wrapClaudeAgentSDK from neatlogs/claude-agent-sdk to trace @anthropic-ai/claude-agent-sdk runs. Pass it the SDK module; it returns a copy with an instrumented query (all other exports pass through unchanged). A query(...) becomes an AGENT root (claude_agent.query) with one LLM span per assistant turn and TOOL spans for each tool use. Subagents spawned via the Task tool nest under their own claude_agent.subagent.<type> span.

Prerequisites

npm install neatlogs@latest @anthropic-ai/claude-agent-sdk

Usage

import { init, flush, shutdown } from 'neatlogs';
import { wrapClaudeAgentSDK } from 'neatlogs/claude-agent-sdk';
import * as claudeAgentSDK from '@anthropic-ai/claude-agent-sdk';

async function main() {
  await init({ apiKey: process.env.NEATLOGS_API_KEY, workflowName: 'claude-agent-demo' });

  // Wrap the SDK module, then use its `query` as usual.
  const { query } = wrapClaudeAgentSDK(claudeAgentSDK);

  for await (const message of query({
    prompt: 'List the files in the current directory and summarize what this project does.',
    options: { model: 'claude-sonnet-4-5' },
  })) {
    if (message.type === 'result') console.log(message.result);
  }

  await flush();
  await shutdown();
}

main().catch(console.error);
Note

The AGENT span is itself a valid trace root, so no WORKFLOW wrapper is needed. Per-turn coalescing keeps one LLM span per model turn with its full input/output and token usage.

Sessions & end-user

Say a user chats with your app across several turns and you want each query() run tracked under one session, credited to the person chatting — each instrumented query opens its own trace root, so wrap the turn in identify() and that root plus every span beneath it picks up the session and end-user:

// Same sessionId every turn → one session; endUserId attributes it to your user.
await identify(
  { sessionId: `conv_${conversationId}`, endUserId: userId },
  async () => {
    for await (const message of query({
      prompt,
      options: { model: 'claude-sonnet-4-5' },
    })) {
      if (message.type === 'result') console.log(message.result);
    }
  },
);

Not every run is a conversation. A standalone workflow — a one-off task, not a back-and-forth — is a single trace with no turns to group. You still attribute it to the customer it ran for, sourcing the id and any metadata from your own user or request object:

Warning

A standalone workflow must not carry a sessionId. Omit it entirely — Neatlogs sets sessionId = traceId, so the run is its own single-turn session, fully attributed to the end-user. Reusing one sessionId across unrelated runs wrongly folds them into a single multi-turn conversation.

async function runTask(prompt, user) {
  // Standalone workflow — attribute to the end-user; do NOT pass sessionId.
  return identify(
    { endUserId: String(user.id), endUserMetadata: { plan: user.plan } },
    async () => {
      for await (const message of query({
        prompt,
        options: { model: 'claude-sonnet-4-5' },
      })) {
        if (message.type === 'result') return message.result;
      }
    },
  );
}

Sessions and End-User Identity walk through the whole model.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

Ask anything about instrumenting, tracing, or the Neatlogs dashboard.