> ## Documentation Index
> Fetch the complete documentation index at: https://arize-ax.mintlify.site/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Claude Managed Agents

> Send OpenTelemetry traces of Claude Managed Agents sessions to Arize AX from the Claude Console and the Claude SDK, with turns, model requests, and tools.

[Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) runs your agent loop on Anthropic's servers and executes its tools in a sandbox that Anthropic hosts. The Claude Platform can [export OpenTelemetry traces](https://platform.claude.com/docs/en/manage-claude/opentelemetry) of each session, using the [OpenTelemetry GenAI semantic conventions](/docs/ax/concepts/otel-openinference/semantic-conventions). Arize AX converts those spans to OpenInference when they arrive. You can then inspect every agent turn, model request, and tool use in a session, with prompts, responses, and token usage.

<Info>
  OpenTelemetry export from the Claude API is now in public beta. Span names and attributes can change during the beta.
</Info>

<Note>
  This page covers Anthropic's **Claude Managed Agents** product. For the managed agents that run inside Arize AX, such as Signal, see [Connect your harness](/docs/ax/agents/connect-your-harness).
</Note>

There are two complementary ways to send spans to Arize AX, and most applications use both:

* **From the Claude Console.** The Claude API exports the spans it creates on Anthropic's servers: the Sessions API requests, agent turns, model requests, and tool uses inside the sandbox. You configure this once for your organization, with no code changes.
* **From your application.** The Claude SDK emits a span for each request it sends and links it to the API's spans with W3C trace context, so a session appears inside your application's own trace.

## Prerequisites

* Python 3.10+, Node.js 18.19+, Go 1.26+, or Java 11+ and Gradle for the application-side example
* An Arize AX account ([sign up](https://arize.com/sign-up/))
* The **admin** role in a Claude Console organization, to add a destination. With the developer role, you can view destinations
* An `ANTHROPIC_API_KEY` from the [Claude Console](https://platform.claude.com/), in a workspace that sends spans to Arize AX

## Launch Arize AX

<Steps>
  <Step title="Sign in">
    Sign in to your [Arize AX account](https://app.arize.com/).
  </Step>

  <Step title="Copy your Space ID and API key">
    From **Space Settings**, copy your **Space ID** and **API Key**. You will set them as `ARIZE_SPACE_ID` and `ARIZE_API_KEY` below.
  </Step>
</Steps>

## Export spans from the Claude Console

A *destination* tells the Claude API where to send the spans it creates. The Claude Console has an Arize provider that fills in the endpoint and headers for you when you select the Arize AX product:

<Steps>
  <Step title="Open the OpenTelemetry settings">
    In the Claude Console, open the [OpenTelemetry settings page](https://platform.claude.com/settings/telemetry-destinations) of your organization and click **Add destination**.
  </Step>

  <Step title="Choose Arize AX">
    Set **Provider** to **Arize**, set **Product** to **Arize AX**, and set **Region** to the region of your Arize AX space.
  </Step>

  <Step title="Enter your Arize AX details">
    Enter your **Space ID** and **API key**. Set **Project name** to the Arize AX project the spans go to, for example `claude-managed-agents-example`.
  </Step>

  <Step title="Set the content mode">
    Set **Content mode** to **Full content**, so spans carry the system prompt, messages, and tool arguments as well as token counts. The default, **Metadata only**, sends only identifiers, timing, status, token counts, and the names and descriptions of your agents, tools, and MCP servers.
  </Step>

  <Step title="Choose the workspaces">
    Set **Workspaces** to the workspaces whose spans you want in Arize AX. **All**, the default, exports from every workspace in your organization. **Selected** exports from only the workspaces you choose, and **All except** exports from every workspace except the ones you choose. A destination exports only requests made with the API keys of its workspaces. Then click **Add destination**.
  </Step>

  <Step title="Send a test span">
    In the destination's **More actions** menu, click **Test**. The Console shows whether the test span was delivered. In Arize AX, check that an `anthropic.test` span arrived in the project.

    <Frame caption="The destination's More actions menu in the Claude Console, with Test highlighted">
      <img src="https://storage.googleapis.com/arize-phoenix-assets/assets/images/arize-docs-images/cookbooks/claude-managed-agents/claude-test-destination.png" alt="The details panel of an Arize AX destination in the Claude Console, showing the endpoint https://otlp.arize.com, content mode Full content, delivery Healthy, and one workspace. The More actions menu is open with Edit, Test, Disable, and Delete, and Test is highlighted." />
    </Frame>
  </Step>
</Steps>

Under **OTLP details**, the Console shows what the destination sends: the endpoint, `https://otlp.arize.com` for US spaces, and the headers `arize-space-id`, `arize-api-key`, and `x-project-name`. The Claude API sets the resource attributes of its spans itself, so Arize AX routes them to a project by the `x-project-name` header.

<Frame caption="The Claude Console's Add destination form with the Arize provider and the Arize AX product">
  <img src="https://storage.googleapis.com/arize-phoenix-assets/assets/images/arize-docs-images/cookbooks/claude-managed-agents/console-arize-destination.png" alt="The Add destination form in the Claude Console with Provider set to Arize, Product set to Arize AX rather than Arize Phoenix, Name set to Arize AX, and Region set to US. The Space ID and API key are filled in and blurred, the project name is claude-managed-agents, and OTLP details are expanded to show the endpoint https://otlp.arize.com and the headers arize-space-id, arize-api-key, and x-project-name. Content mode is set to Full content, Workspaces is set to All rather than Selected or All except, and Advanced settings is collapsed." />
</Frame>

<Warning>
  A destination exports every request made with its workspaces' API keys, not just Managed Agents sessions. That includes Messages API requests from any other application or tool that uses those keys, with their prompts and responses in **Full content** mode. To send only your agent's traffic, give it a workspace and API keys of its own. **Workspaces** defaults to **All**, so choose **Selected** unless you want spans from every workspace in your organization in Arize AX.
</Warning>

Each destination sends to one Arize AX project. To keep workspaces apart, for example production and staging, add one destination per project and select each destination's workspaces. An organization can have up to 10 destinations, including disabled ones.

An admin can also add a destination with Anthropic's [Admin API or CLI](https://platform.claude.com/docs/en/manage-claude/opentelemetry/api#admin-api-and-cli). The Admin API has no Arize provider, so enter the endpoint and headers yourself: the endpoint `https://otlp.arize.com`, or `https://otlp.eu-west-1a.arize.com` for EU spaces, and the headers `arize-space-id`, `arize-api-key`, and `x-project-name`.

## Install

The rest of this page traces a Managed Agents session from your application. You register an OpenTelemetry tracer provider that exports to Arize AX, then run the session inside one of your own spans. The Claude SDK sends the trace context of that span with each request. The API's spans then nest under it, and the whole session appears as one trace.

<CodeGroup>
  ```bash Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  pip install arize-otel anthropic
  ```

  ```bash TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  npm install @anthropic-ai/sdk \
    @opentelemetry/api \
    @opentelemetry/exporter-trace-otlp-proto \
    @opentelemetry/resources \
    @opentelemetry/sdk-trace-base \
    @opentelemetry/sdk-trace-node \
    @opentelemetry/semantic-conventions \
    @arizeai/openinference-core \
    @arizeai/openinference-semantic-conventions \
    tsx
  ```

  ```bash Go theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  go get \
    github.com/Arize-ai/arize-otel-go \
    github.com/anthropics/anthropic-sdk-go
  ```

  ```groovy Java theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  // build.gradle
  plugins {
      id 'application'
  }

  repositories {
      mavenCentral()
  }

  dependencies {
      implementation 'com.anthropic:anthropic-java:2.72.0'
      implementation 'io.opentelemetry:opentelemetry-sdk:1.66.0'
      implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.66.0'
  }

  application {
      mainClass = 'example.Main'
  }
  ```
</CodeGroup>

[Install or upgrade the Claude SDK](https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk#install-the-sdk) to a version that emits spans. The SDK emits a span for each request and sends the trace context that joins the API's spans to yours. With an older SDK, the API's spans still reach Arize AX from the Console destination, in traces of their own.

<Warning>
  Don't also run the [OpenInference Anthropic instrumentor](/docs/ax/integrations/llm-providers/anthropic/anthropic-tracing) on the same client. Both would trace each request, and every request would appear twice.
</Warning>

## Configure credentials

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
export ARIZE_SPACE_ID="<your-space-id>"
export ARIZE_API_KEY="<your-api-key>"
export ARIZE_PROJECT_NAME="claude-managed-agents-example"
export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
export ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE="content"
```

Use the same project name here as the **Project name** of your destination, so the SDK's spans and the API's spans land in the same project. The examples send to the US endpoint by default. For a space in another region, also set `ARIZE_COLLECTOR_ENDPOINT` to that region's endpoint, for example `https://otlp.eu-west-1a.arize.com/v1` for EU spaces, and select the same **Region** on the destination. `ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content` makes the SDK spans carry prompts and responses. Without it, they carry only metadata and token counts. The Java client reads it when you build it with `AnthropicOkHttpClient.fromEnv()`.

## Setup tracing

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  # instrumentation.py
  from arize.otel import register

  # register() reads ARIZE_SPACE_ID, ARIZE_API_KEY, ARIZE_PROJECT_NAME, and
  # ARIZE_COLLECTOR_ENDPOINT from the environment.
  tracer_provider = register()

  print("Arize AX tracing initialized for Claude Managed Agents.")
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  // instrumentation.ts
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
  import { resourceFromAttributes } from "@opentelemetry/resources";
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
  import {
    SEMRESATTRS_PROJECT_NAME,
  } from "@arizeai/openinference-semantic-conventions";

  const projectName = process.env.ARIZE_PROJECT_NAME ?? "default";
  // For EU or Canada spaces, set ARIZE_COLLECTOR_ENDPOINT to your region's
  // endpoint, such as https://otlp.eu-west-1a.arize.com/v1.
  const endpoint =
    process.env.ARIZE_COLLECTOR_ENDPOINT ?? "https://otlp.arize.com/v1";

  export const provider = new NodeTracerProvider({
    resource: resourceFromAttributes({
      [ATTR_SERVICE_NAME]: projectName,
      [SEMRESATTRS_PROJECT_NAME]: projectName,
    }),
    spanProcessors: [
      new BatchSpanProcessor(
        new OTLPTraceExporter({
          url: `${endpoint}/traces`,
          headers: {
            "arize-api-key": process.env.ARIZE_API_KEY ?? "",
            "arize-space-id": process.env.ARIZE_SPACE_ID ?? "",
          },
        }),
      ),
    ],
  });

  // register() installs the W3C trace context propagator and the context
  // manager, so the SDK's spans nest under your own spans.
  provider.register();

  console.log("Arize AX tracing initialized for Claude Managed Agents.");
  ```

  ```go Go theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  // main.go
  package main

  import (
      "context"
      "fmt"
      "log"
      "os"
      "strings"
      "time"

      arizeotel "github.com/Arize-ai/arize-otel-go"
      "github.com/anthropics/anthropic-sdk-go"
      "go.opentelemetry.io/otel"
      "go.opentelemetry.io/otel/attribute"
      "go.opentelemetry.io/otel/codes"
      "go.opentelemetry.io/otel/propagation"
      "go.opentelemetry.io/otel/trace"
  )

  const prompt = "Use bash to compute the first 10 Fibonacci numbers. " +
      "Reply with only the numbers, comma-separated."

  func main() {
      if err := run(context.Background()); err != nil {
          log.Fatal(err)
      }
  }

  // run returns errors instead of exiting, so the deferred calls end the
  // span and flush it to Arize AX even when a request fails.
  func run(ctx context.Context) (err error) {
      // Register reads ARIZE_SPACE_ID, ARIZE_API_KEY, and
      // ARIZE_COLLECTOR_ENDPOINT from the environment.
      tp, err := arizeotel.Register(ctx, arizeotel.Options{
          ProjectName: os.Getenv("ARIZE_PROJECT_NAME"),
      })
      if err != nil {
          return fmt.Errorf("register tracer: %w", err)
      }
      defer func() {
          shutdownCtx, cancel := context.WithTimeout(
              context.Background(), 5*time.Second)
          defer cancel()
          _ = tp.Shutdown(shutdownCtx)
      }()
      // OpenTelemetry for Go registers no propagator by default. The Claude
      // SDK needs W3C trace context to link its spans to the API's spans.
      otel.SetTextMapPropagator(propagation.TraceContext{})

      fmt.Println("Arize AX tracing initialized for Claude Managed Agents.")

      // The client reads ANTHROPIC_API_KEY from the environment.
      client := anthropic.NewClient()

      environment, err := client.Beta.Environments.New(ctx,
          anthropic.BetaEnvironmentNewParams{
              Name: "arize-tracing-example",
              Config: anthropic.BetaEnvironmentNewParamsConfigUnion{
                  OfCloud: &anthropic.BetaCloudConfigParams{
                      Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{
                          OfLimited: &anthropic.BetaLimitedNetworkParams{},
                      },
                  },
              },
          })
      if err != nil {
          return fmt.Errorf("create environment: %w", err)
      }

      agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
          Name: "Fibonacci Agent",
          Model: anthropic.BetaManagedAgentsModelConfigParams{
              ID: "claude-opus-5-5",
          },
          System: anthropic.String(
              "You are a concise assistant. Use bash to compute answers."),
          Tools: []anthropic.BetaAgentNewParamsToolUnion{{
              OfAgentToolset20260401: toolset(),
          }},
      })
      if err != nil {
          return fmt.Errorf("create agent: %w", err)
      }

      ctx, span := otel.Tracer("claude-managed-agents-example").
          Start(ctx, "fibonacci-agent", trace.WithAttributes(
              attribute.String("openinference.span.kind", "CHAIN"),
              attribute.String("input.value", prompt),
          ))
      defer func() {
          if err != nil {
              span.RecordError(err)
              span.SetStatus(codes.Error, err.Error())
          } else {
              span.SetStatus(codes.Ok, "")
          }
          span.End()
      }()

      sessionParams := anthropic.BetaSessionNewParams{
          Agent: anthropic.BetaSessionNewParamsAgentUnion{
              OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{
                  Type:    anthropic.BetaManagedAgentsAgentParamsTypeAgent,
                  ID:      agent.ID,
                  Version: anthropic.Int(agent.Version),
              },
          },
          EnvironmentID: environment.ID,
      }
      session, err := client.Beta.Sessions.New(ctx, sessionParams)
      if err != nil {
          return fmt.Errorf("create session: %w", err)
      }

      // Open the stream before sending the message so no events are missed.
      stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID,
          anthropic.BetaSessionEventStreamParams{})
      defer stream.Close()

      message := anthropic.BetaManagedAgentsUserMessageEventParams{
          Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
          Content: []anthropic.
              BetaManagedAgentsUserMessageEventParamsContentUnion{{
              OfText: &anthropic.BetaManagedAgentsTextBlockParam{
                  Type: anthropic.BetaManagedAgentsTextBlockTypeText,
                  Text: prompt,
              },
          }},
      }
      _, err = client.Beta.Sessions.Events.Send(ctx, session.ID,
          anthropic.BetaSessionEventSendParams{
              Events: []anthropic.BetaManagedAgentsEventParamsUnion{
                  {OfUserMessage: &message},
              },
          })
      if err != nil {
          return fmt.Errorf("send message: %w", err)
      }

      var replies []string
  events:
      for stream.Next() {
          switch event := stream.Current().AsAny().(type) {
          case anthropic.BetaManagedAgentsAgentMessageEvent:
              for _, block := range event.Content {
                  replies = append(replies, block.Text)
              }
          case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
              if event.StopReason.Type != "requires_action" {
                  break events
              }
          case anthropic.BetaManagedAgentsSessionStatusTerminatedEvent:
              break events
          }
      }
      if err := stream.Err(); err != nil {
          return fmt.Errorf("stream: %w", err)
      }

      answer := strings.Join(replies, "\n")
      span.SetAttributes(attribute.String("output.value", answer))
      fmt.Println(answer)
      return nil
  }

  func toolset() *anthropic.BetaManagedAgentsAgentToolset20260401Params {
      ts := &anthropic.BetaManagedAgentsAgentToolset20260401Params{
          Configs: []anthropic.BetaManagedAgentsAgentToolConfigParamsUnion{
              {OfWebFetch: &anthropic.BetaManagedAgentsWebFetchToolConfigParams{
                  Enabled: anthropic.Bool(false),
              }},
              {OfWebSearch: &anthropic.BetaManagedAgentsWebSearchToolConfigParams{
                  Enabled: anthropic.Bool(false),
              }},
          },
      }
      ts.Type = anthropic.
          BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401
      return ts
  }
  ```

  ```java Java theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  // src/main/java/example/Main.java
  package example;

  import com.anthropic.client.AnthropicClient;
  import com.anthropic.client.okhttp.AnthropicOkHttpClient;
  import com.anthropic.models.beta.agents.*;
  import com.anthropic.models.beta.environments.BetaCloudConfigParams;
  import com.anthropic.models.beta.environments.BetaLimitedNetworkParams;
  import com.anthropic.models.beta.environments.EnvironmentCreateParams;
  import com.anthropic.models.beta.sessions.BetaManagedAgentsAgentParams;
  import com.anthropic.models.beta.sessions.SessionCreateParams;
  import com.anthropic.models.beta.sessions.events.*;
  import io.opentelemetry.api.common.AttributeKey;
  import io.opentelemetry.api.common.Attributes;
  import io.opentelemetry.api.trace.Span;
  import io.opentelemetry.api.trace.StatusCode;
  import io.opentelemetry.api.trace.Tracer;
  import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
  import io.opentelemetry.context.Scope;
  import io.opentelemetry.context.propagation.ContextPropagators;
  import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
  import io.opentelemetry.sdk.OpenTelemetrySdk;
  import io.opentelemetry.sdk.resources.Resource;
  import io.opentelemetry.sdk.trace.SdkTracerProvider;
  import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
  import java.net.URI;
  import java.util.ArrayList;
  import java.util.Map;
  import java.util.concurrent.TimeUnit;

  public class Main {

      private static final String PROMPT =
          "Use bash to compute the first 10 Fibonacci numbers. "
              + "Reply with only the numbers, comma-separated.";

      public static void main(String[] args) {
          String project = System.getenv().getOrDefault(
              "ARIZE_PROJECT_NAME", "default");

          Resource resource = Resource.getDefault().merge(Resource.create(
              Attributes.of(
                  AttributeKey.stringKey("service.name"), project,
                  AttributeKey.stringKey("openinference.project.name"),
                  project)));

          // For EU or Canada spaces, set ARIZE_COLLECTOR_ENDPOINT to your
          // region's endpoint, such as https://otlp.eu-west-1a.arize.com/v1.
          URI collector = URI.create(System.getenv().getOrDefault(
              "ARIZE_COLLECTOR_ENDPOINT", "https://otlp.arize.com/v1"));
          OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
              .setEndpoint("https://" + collector.getHost() + ":443")
              .setHeaders(() -> Map.of(
                  "authorization", System.getenv("ARIZE_API_KEY"),
                  "arize-space-id", System.getenv("ARIZE_SPACE_ID")))
              .build();

          SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
              .addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
              .setResource(resource)
              .build();

          // The Claude SDK reads the global OpenTelemetry instance. It needs
          // the W3C propagator to link its spans to the API's spans.
          OpenTelemetrySdk otel = OpenTelemetrySdk.builder()
              .setTracerProvider(tracerProvider)
              .setPropagators(ContextPropagators.create(
                  W3CTraceContextPropagator.getInstance()))
              .buildAndRegisterGlobal();

          System.out.println(
              "Arize AX tracing initialized for Claude Managed Agents.");

          // fromEnv() reads ANTHROPIC_API_KEY and the
          // ANTHROPIC_OPEN_TELEMETRY_* variables.
          AnthropicClient client = AnthropicOkHttpClient.fromEnv();
          Tracer tracer = otel.getTracer("claude-managed-agents-example");

          var environment = client.beta().environments().create(
              EnvironmentCreateParams.builder()
                  .name("arize-tracing-example")
                  .config(BetaCloudConfigParams.builder()
                      .networking(BetaLimitedNetworkParams.builder().build())
                      .build())
                  .build());

          var agent = client.beta().agents().create(AgentCreateParams.builder()
              .name("Fibonacci Agent")
              .model(BetaManagedAgentsModel.of("claude-opus-5-5"))
              .system("You are a concise assistant. "
                  + "Use bash to compute answers.")
              .addTool(BetaManagedAgentsAgentToolset20260401Params.builder()
                  .type(BetaManagedAgentsAgentToolset20260401Params.Type
                      .AGENT_TOOLSET_20260401)
                  .addConfig(BetaManagedAgentsWebFetchToolConfigParams.builder()
                      .enabled(false).build())
                  .addConfig(BetaManagedAgentsWebSearchToolConfigParams.builder()
                      .enabled(false).build())
                  .build())
              .build());

          Span span = tracer.spanBuilder("fibonacci-agent").startSpan();
          try (Scope scope = span.makeCurrent()) {
              span.setAttribute("openinference.span.kind", "CHAIN");
              span.setAttribute("input.value", PROMPT);

              var session = client.beta().sessions().create(
                  SessionCreateParams.builder()
                      .agent(BetaManagedAgentsAgentParams.builder()
                          .type(BetaManagedAgentsAgentParams.Type.AGENT)
                          .id(agent.id())
                          .version(agent.version())
                          .build())
                      .environmentId(environment.id())
                      .build());

              var replies = new ArrayList<String>();

              // Open the stream before sending the message so no events
              // are missed.
              try (var stream = client.beta().sessions().events()
                      .streamStreaming(session.id())) {
                  client.beta().sessions().events().send(session.id(),
                      EventSendParams.builder()
                          .addEvent(BetaManagedAgentsUserMessageEventParams
                              .builder()
                              .type(BetaManagedAgentsUserMessageEventParams
                                  .Type.USER_MESSAGE)
                              .addTextContent(PROMPT)
                              .build())
                          .build());

                  Iterable<BetaManagedAgentsStreamSessionEvents> events =
                      stream.stream()::iterator;
                  for (var event : events) {
                      if (event.isAgentMessage()) {
                          event.asAgentMessage().content()
                              .forEach(block -> block.text().ifPresent(
                                  text -> replies.add(text.text())));
                      } else if (event.isSessionStatusIdle()) {
                          if (!event.asSessionStatusIdle().stopReason()
                                  .isRequiresAction()) {
                              break;
                          }
                      } else if (event.isSessionStatusTerminated()) {
                          break;
                      }
                  }
              }

              String answer = String.join("\n", replies);
              span.setAttribute("output.value", answer);
              span.setStatus(StatusCode.OK);
              System.out.println(answer);
          } catch (Exception e) {
              span.recordException(e);
              span.setStatus(StatusCode.ERROR, e.getMessage());
              throw e;
          } finally {
              span.end();
          }

          tracerProvider.forceFlush().join(10, TimeUnit.SECONDS);
          tracerProvider.shutdown().join(10, TimeUnit.SECONDS);
      }
  }
  ```
</CodeGroup>

Go and Java keep the tracer setup and the example in one file, with the setup at the top of `main`. Neither registers the W3C trace context propagator by default, so both examples register it. Without it, the SDK can't link its spans to the API's spans. The Java example also registers the OpenTelemetry SDK globally, because the Claude SDK for Java uses the global instance.

## Run Claude Managed Agents

The example creates an environment and an agent, starts a session inside a `fibonacci-agent` span, and asks the agent to compute Fibonacci numbers with bash. It prints the agent's reply. Run the Python example with `python example.py` and the TypeScript example with `npx tsx example.ts`.

The `fibonacci-agent` span is a `CHAIN` span, with the request as its input and the agent's reply as its output. Python uses OpenInference's `@tracer.chain` decorator and TypeScript its `traceChain` wrapper, which set the span kind, record the input and output, and set the span's status for you. Go and Java set the `openinference.span.kind`, `input.value`, and `output.value` attributes themselves, and set the status to `OK` when the session finishes. Without a status, Arize AX shows a span's status as unset. Without a span kind, Arize AX shows a span's kind as unknown.

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  # example.py

  # Importing instrumentation first registers the tracer provider
  # before the Claude SDK makes its first request.
  from instrumentation import tracer_provider

  import anthropic

  # The client reads ANTHROPIC_API_KEY from the environment.
  client = anthropic.Anthropic()
  tracer = tracer_provider.get_tracer("claude-managed-agents-example")

  REQUEST = (
      "Use bash to compute the first 10 Fibonacci numbers. "
      "Reply with only the numbers, comma-separated."
  )

  environment = client.beta.environments.create(
      name="arize-tracing-example",
      config={"type": "cloud", "networking": {"type": "limited"}},
  )

  agent = client.beta.agents.create(
      name="Fibonacci Agent",
      model="claude-opus-5-5",
      system="You are a concise assistant. Use bash to compute answers.",
      tools=[
          {
              "type": "agent_toolset_20260401",
              "configs": [
                  {"name": "web_fetch", "enabled": False},
                  {"name": "web_search", "enabled": False},
              ],
          },
      ],
  )

  @tracer.chain(name="fibonacci-agent")
  def run_agent(request: str) -> str:
      session = client.beta.sessions.create(
          agent={"type": "agent", "id": agent.id, "version": agent.version},
          environment_id=environment.id,
      )
      replies = []

      # Open the stream before sending the message so no events are missed.
      with client.beta.sessions.events.stream(session_id=session.id) as stream:
          client.beta.sessions.events.send(
              session_id=session.id,
              events=[
                  {
                      "type": "user.message",
                      "content": [{"type": "text", "text": request}],
                  },
              ],
          )

          for event in stream:
              if event.type == "agent.message":
                  for block in event.content:
                      if block.type == "text":
                          replies.append(block.text)
              elif event.type == "session.status_idle":
                  if event.stop_reason.type != "requires_action":
                      break
              elif event.type == "session.status_terminated":
                  break

      return "\n".join(replies)


  print(run_agent(REQUEST))
  tracer_provider.shutdown()
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  // example.ts

  // Importing instrumentation first registers the tracer provider
  // before the Claude SDK makes its first request.
  import { provider } from "./instrumentation";
  import Anthropic from "@anthropic-ai/sdk";
  import { traceChain } from "@arizeai/openinference-core";

  // The client reads ANTHROPIC_API_KEY from the environment.
  const client = new Anthropic();

  const REQUEST =
    "Use bash to compute the first 10 Fibonacci numbers. " +
    "Reply with only the numbers, comma-separated.";

  async function main() {
    const environment = await client.beta.environments.create({
      name: "arize-tracing-example",
      config: { type: "cloud", networking: { type: "limited" } },
    });

    const agent = await client.beta.agents.create({
      name: "Fibonacci Agent",
      model: "claude-opus-5-5",
      system: "You are a concise assistant. Use bash to compute answers.",
      tools: [
        {
          type: "agent_toolset_20260401",
          configs: [
            { name: "web_fetch", enabled: false },
            { name: "web_search", enabled: false },
          ],
        },
      ],
    });

    const runAgent = traceChain(
      async (request: string): Promise<string> => {
        const session = await client.beta.sessions.create({
          agent: { type: "agent", id: agent.id, version: agent.version },
          environment_id: environment.id,
        });
        const replies: string[] = [];

        // Open the stream before sending the message so no events are missed.
        const stream = await client.beta.sessions.events.stream(session.id);
        await client.beta.sessions.events.send(session.id, {
          events: [
            { type: "user.message", content: [{ type: "text", text: request }] },
          ],
        });

        loop: for await (const event of stream) {
          switch (event.type) {
            case "agent.message":
              for (const block of event.content) {
                if (block.type === "text") replies.push(block.text);
              }
              break;
            case "session.status_idle":
              if (event.stop_reason.type !== "requires_action") break loop;
              break;
            case "session.status_terminated":
              break loop;
          }
        }

        return replies.join("\n");
      },
      { name: "fibonacci-agent" },
    );

    console.log(await runAgent(REQUEST));
    await provider.shutdown();
  }

  main().catch((err) => {
    console.error(err);
    process.exit(1);
  });
  ```

  ```bash Go theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  # The full example is in main.go above.
  go run main.go
  ```

  ```bash Java theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  # The full example is in Main.java above.
  gradle run
  ```
</CodeGroup>

<Note>
  Agents and environments are persistent and versioned. The example creates them on every run to stay self-contained. In your application, create them once, store their IDs, and pass the IDs to each `sessions.create` call.
</Note>

### Expected output

```text wrap theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
Arize AX tracing initialized for Claude Managed Agents.
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
```

## Verify in Arize AX

<Steps>
  <Step title="Open the project">
    Open your Arize AX space and select project **`claude-managed-agents-example`**.
  </Step>

  <Step title="Open the new trace">
    You should see a new trace within about a minute. The API's spans can arrive after yours. The trace has a `fibonacci-agent` `CHAIN` root span, the SDK's client spans for each Sessions API request, and the API's spans under them: an `anthropic.session.turn` span with `anthropic.model_request` and tool spans inside it.

    If no traces appear, see [Troubleshooting](#troubleshooting).
  </Step>
</Steps>

<Frame caption="A Claude Managed Agents session in Arize AX, with the agent's turn selected">
  <img src="https://storage.googleapis.com/arize-phoenix-assets/assets/images/arize-docs-images/cookbooks/claude-managed-agents/fibonacci-trace.png" alt="Arize AX trace view of a fibonacci-agent trace with status OK. The tree has the fibonacci-agent CHAIN span at the root, the SDK's anthropic.sessions.create, anthropic.sessions.events.stream, and anthropic.sessions.events.send spans, each with the API's span of the same name inside, then an anthropic.session.turn span containing two anthropic.model_request spans and an anthropic.tool_use bash span. The selected turn shows the system prompt, the user's request for the first 10 Fibonacci numbers, the agent's bash call, the tool result, and the agent's reply." />
</Frame>

On the Console's OpenTelemetry settings page, the **Delivery** column for your destination also shows **Healthy** once the API delivers its first spans to Arize AX.

### Check from the skill, CLI, or SDK

Confirm spans are actually reaching your Arize AX project. Use whichever fits your workflow — the skill and CLI work for any framework; the SDK check is shown for each language.

<Tabs>
  <Tab title="Arize skill (agent)">
    Install the [Arize Skills](https://github.com/Arize-ai/arize-skills) plugin and let your coding agent check for you:

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    npx skills add Arize-ai/arize-skills
    ```

    Then prompt your agent:

    > Use the `arize-trace` skill to export and analyze recent traces from my project. Confirm spans are arriving, and summarize any errors or latency issues.
  </Tab>

  <Tab title="AX CLI">
    Export recent spans for your project — any rows mean traces are landing:

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    ax spans export "$ARIZE_PROJECT_NAME" --space "$ARIZE_SPACE_ID" \
      --limit 5 --stdout | jq 'length'
    ```

    A non-zero count confirms spans reached Arize AX. Run `ax auth login` first if you have not authenticated. See the [`ax spans` reference](/docs/api-clients/cli/spans).
  </Tab>

  <Tab title="SDK">
    Query the project's spans and check that at least one came back.

    <CodeGroup>
      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import os
      from arize import ArizeClient

      client = ArizeClient(api_key=os.environ["ARIZE_API_KEY"])
      resp = client.spans.list(
          project=os.environ["ARIZE_PROJECT_NAME"],
          space=os.environ["ARIZE_SPACE_ID"],
          limit=5,
      )
      count = len(resp.spans)
      print(
          f"{count} span(s) found" if count else "No spans yet — recheck setup"
      )
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      // Reads ARIZE_API_KEY from the environment.
      import { listSpans } from "@arizeai/ax-client";

      const { data: spans } = await listSpans({
        project: process.env.ARIZE_PROJECT_NAME!,
        space: process.env.ARIZE_SPACE_ID!,
        limit: 5,
      });
      const count = spans.length;
      console.log(
        count ? `${count} span(s) found` : "No spans yet — recheck setup",
      );
      ```

      ```go Go theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      client, err := arize.NewClient(
          arize.Config{APIKey: os.Getenv("ARIZE_API_KEY")},
      )
      if err != nil {
          log.Fatal(err)
      }
      resp, err := client.Spans.List(ctx, spans.ListRequest{
          Project: os.Getenv("ARIZE_PROJECT_NAME"),
          Space:   os.Getenv("ARIZE_SPACE_ID"),
          Limit:   5,
      })
      if err != nil {
          log.Fatal(err)
      }
      fmt.Printf("%d span(s) found\n", len(resp.Spans))
      ```
    </CodeGroup>

    SDK span references: [Python](/docs/api-clients/python/version-8/client-resources/spans) · [TypeScript](/docs/api-clients/typescript/version-1/client-resources/spans) · [Go](/docs/api-clients/go/version-2/client-resources/spans).
  </Tab>
</Tabs>

## Add tracing to your application

To trace your own Managed Agents application, follow the same steps as the example:

<Steps>
  <Step title="Add a Console destination">
    An admin of your Claude Console organization adds the destination in the Console or with the Admin API, as in [Export spans from the Claude Console](#export-spans-from-the-claude-console). Without it, none of the API's spans reach Arize AX.
  </Step>

  <Step title="Install the packages">
    Install the packages from [Install](#install), and [install or upgrade the Claude SDK](https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk#install-the-sdk) to a version that emits spans. Don't add the OpenInference Anthropic instrumentor: the Claude SDK traces its own requests.
  </Step>

  <Step title="Register the tracer provider">
    Register the tracer provider before you create the Claude client, with the destination's project name and your space's region.
  </Step>

  <Step title="Run each session inside a CHAIN span">
    Create a `CHAIN` span for each session. Wrap the function that creates the session, sends the user's message, and reads the event stream, with the message as the input and the agent's reply as the output. In Python, use the `@tracer.chain` decorator, and in TypeScript, the `traceChain` wrapper. In Go and Java, set `openinference.span.kind`, `input.value`, `output.value`, and the span's status yourself.
  </Step>

  <Step title="Run each custom tool inside a TOOL span">
    Create a `TOOL` span for each custom tool that your application runs. A custom tool runs in your application, so the Claude API can't see inside it. In Python, use `@tracer.tool`, and in TypeScript, `traceTool`. In Go and Java, also set `tool.name`, `tool.description`, and `tool.parameters`. The API already traces the tools that run in its sandbox, such as bash and MCP tools, so don't add spans for those. In a [self-hosted sandbox](https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk#trace-a-self-hosted-sandbox), the SDK's environment worker traces those tools instead, under an `anthropic.environment.work` span for each work item. If you run your custom tools with the Claude SDK's session tool runner, `client.beta.sessions.events.tool_runner` in Python, `client.beta.sessions.events.toolRunner` in TypeScript, or `client.Beta.Sessions.Events.NewToolRunner` in Go, the SDK already creates a `TOOL` span for each call, `anthropic.custom_tool_use <tool>`, so don't add your own. The [cookbook](/docs/ax/cookbooks/instrument/claude-managed-agents-guide) shows a custom tool in all four languages.
  </Step>

  <Step title="Shut down the tracer provider">
    Shut down the tracer provider before your process exits, so that it exports the last spans. The examples do this at the end of the program.
  </Step>
</Steps>

You don't need to set `session.id`: Arize AX groups each thread's spans into a session by `gen_ai.conversation.id`, as described in [What Arize AX captures](#what-arize-ax-captures). Some of the Claude SDK's and the API's spans show an unknown kind or unset status, as listed in [Known limitations](#known-limitations). They don't mean your instrumentation is wrong.

## What Arize AX captures

The SDK and the API mark their spans with `gen_ai.*` attributes, and Arize AX converts them to OpenInference attributes on ingestion. See [How Arize AX ingests GenAI spans](/docs/ax/concepts/otel-openinference/semantic-conventions#how-arize-ax-ingests-genai-spans).

| Span | From | What it shows in Arize AX |
| - | - | - |
| `anthropic.sessions.*`, `anthropic.agents.*`, `anthropic.environments.*` | SDK (`CLIENT`) | Each request the SDK sends, with its IDs, status, and timing. Requests that you make outside your own spans, such as creating the agent before the session, are in traces of their own |
| `anthropic.sessions.*` | API (`SERVER`) | Each Sessions API request, as the API handled it, nested under the SDK's span with the same name. Turns that the request starts are its children |
| `anthropic.session.turn` | API | An `AGENT` span for one agent turn: the system prompt, the turn's input and output messages, its stop reason, and its child spans |
| `anthropic.model_request` | API | An `LLM` span for one model request, with the model and token usage. The messages are on the turn span |
| `anthropic.tool_use <tool>`, `anthropic.mcp_tool_use <tool>` | API, or SDK | A `TOOL` span for a built-in or MCP tool, such as `anthropic.tool_use bash`, with its arguments as the input. The API emits it for tools in its sandbox, and the SDK for tools in a self-hosted sandbox and for MCP tools that a tool runner runs |
| `anthropic.custom_tool_use <tool>` | SDK | A `TOOL` span for a custom tool that your application ran, when you use the SDK's session tool runner, with the tool's name, description, and arguments as the input |
| `anthropic.session.outcome_evaluation` | API | A grader evaluation of an [outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes) |
| `anthropic.environment.work` | SDK | One work item that a [self-hosted sandbox](https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk#trace-a-self-hosted-sandbox)'s worker ran, with the worker's tool spans and requests inside it |

No span shows a custom tool that your application runs without a tool runner, a multiagent or advisor tool, or a tool use that the permission policy or your application denied. Add your own `TOOL` span for a custom tool, as in [Add tracing to your application](#add-tracing-to-your-application).

Arize AX groups a session's spans into a [session](/docs/ax/instrument/set-up-sessions) by `gen_ai.conversation.id`, which holds the session thread ID, so each Managed Agents thread is one session in the **Sessions** tab. Select it to read the thread as a conversation. If you set the SDK's `conversation_id` option, the SDK's spans carry your ID in `gen_ai.conversation.id`, so they form a session of their own, apart from the API's spans. To find every span of a multiagent session across all its [threads](https://platform.claude.com/docs/en/managed-agents/session-threads), filter on `anthropic.session.id`. A turn that starts because an agent on another thread replied or finished has no parent, so it's the root of a trace of its own.

### Known limitations

* **Cache token counts aren't shown.** The spans report cache reads and writes as `gen_ai.usage.cache_read.input_tokens` and `gen_ai.usage.cache_write.input_tokens`, which Arize AX doesn't map yet. Arize AX counts all of a request's input tokens as regular prompt tokens, so cost estimates for requests that read from the prompt cache can be higher than what you were billed. The raw attributes stay on the span. For a session's actual token usage and list cost, read the session's `usage` field or the `session.usage` event, as described in Anthropic's [Inspect sessions and track usage](https://platform.claude.com/docs/en/managed-agents/session-observability#track-usage).
* **Session requests appear as LLM spans.** The `anthropic.sessions.create` spans carry the agent's model, so Arize AX classifies them as `LLM` spans, with zero tokens. They aren't model calls. The model calls are the `anthropic.model_request` spans.
* **Some Sessions API requests have no span kind.** The `anthropic.agents.create`, `anthropic.environments.create`, `anthropic.sessions.events.send`, and `anthropic.sessions.events.stream` spans don't say what kind of operation they are, so Arize AX shows their kind as unknown. Your own spans don't have this problem as long as they set a span kind.
* **The SDK's stream spans have no status.** The SDK's `anthropic.sessions.events.stream` span ends without a status when your application stops reading the stream, so Arize AX shows it as unset. The API's span of the same name inside it has a status.
* **Messages API requests count tokens more than once.** For a Managed Agents session, only the `anthropic.model_request` and `anthropic.session.outcome_evaluation` spans carry token usage, so trace totals are correct. For a Messages API request, the usage is on the SDK's `anthropic.messages.create` span, the API's span of the same name, and the API's `anthropic.model_request` children, so trace totals are higher than the request used. See Anthropic's guidance on [counting tokens](https://platform.claude.com/docs/en/manage-claude/opentelemetry#count-tokens).
* **The Sessions list doesn't summarize these sessions.** The session ID is on the SDK's and the API's spans, not on the `CHAIN` span at the root of each trace, so the Sessions list shows no first message, last message, duration, or tokens, and a session shows zero traces. Select the session to read its conversation.
* **Tool spans don't show an output.** Tool results are in the `anthropic.tool_result.content` attribute, which Arize AX doesn't map to the span output yet. Read it from the span's attributes. On the SDK's `anthropic.custom_tool_use` spans, `tool.parameters` also holds the call's arguments rather than the tool's JSON schema.
* **Images and documents aren't included.** Spans record only the type of an image or document in a prompt or response, not its data.
* **Long content is shortened.** Each span holds a limited amount of content, by default 0.75 MB. When content is over the limit, the SDK or the API shortens it and sets `anthropic.content.truncated` to `true`. Change the destination's limit under **Advanced settings**, and the SDK's with `ANTHROPIC_OPEN_TELEMETRY_TRACES_MAX_CONTENT_BYTES`. See Anthropic's [Content limits](https://platform.claude.com/docs/en/manage-claude/opentelemetry#content-limits).

## Troubleshooting

* **No traces at all.** Check that `ARIZE_SPACE_ID`, `ARIZE_API_KEY`, and `ARIZE_PROJECT_NAME` are set in the shell that runs the example. Without `ARIZE_PROJECT_NAME`, your spans go to a project named `default`. Register the tracer provider before the first Claude SDK call, and check that `ANTHROPIC_OPEN_TELEMETRY` isn't `false`. Set `OTEL_LOG_LEVEL=debug` to see export errors.
* **The destination shows Failing.** Hold the pointer over **Failing** to see the error. A `401` or `403` means a wrong **API key** or **Space ID**, or a key that doesn't belong to that space. A rejection that mentions project routing means the destination has no **Project name**. Check that the **Region** matches your space: EU spaces use `https://otlp.eu-west-1a.arize.com`. If your own spans don't arrive either, check that `ARIZE_COLLECTOR_ENDPOINT` points to the same region. If you change the **Region** of a destination, enter the **API key** again.
* **The destination shows Disabled.** Hold the pointer over **Disabled** to see why. The API disables a destination after 24 hours of failed deliveries, or when the endpoint redirects or resolves to an IP address that isn't public. Fix the cause, then click **Enable** in the destination's **More actions** menu, with the **Workspace** filter set to **All**.
* **Only your own spans appear, with no API spans.** Check that the destination's **Workspaces** include the workspace of your `ANTHROPIC_API_KEY`, and that the destination's **Project name** matches `ARIZE_PROJECT_NAME`. The API's spans can take about a minute to arrive.
* **The API's spans are in a separate trace.** The SDK didn't send the trace context. Check that you [installed or upgraded the Claude SDK](https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk#install-the-sdk) to a version that emits spans. In Go and Java, register the W3C trace context propagator as the examples do, and in TypeScript call `provider.register()`. If your application uses another propagator, such as B3, add the W3C trace context propagator too. In Java, also build the OpenTelemetry SDK with `buildAndRegisterGlobal()`, and check that no other dependency pins `opentelemetry-api` below 1.57.0. In TypeScript, if you use `@opentelemetry/instrumentation-undici`, which `@opentelemetry/auto-instrumentations-node` includes, create the client with `openTelemetry: { propagation: false }`: that package adds a second `traceparent` header, which makes the trace context invalid. Requests to Claude Platform on AWS always start traces of their own.
* **Your spans are missing when the program exits.** The batch span processor exports in the background. Shut down the tracer provider before the process exits, as the examples do: `shutdown()` in Python and TypeScript, the deferred `Shutdown` in Go, and `forceFlush` and `shutdown` in Java. In Go, return errors from a function instead of calling `log.Fatal` while a span is open, because `log.Fatal` exits without running deferred calls.
* **Each request appears twice.** Each request has an SDK span and, inside it, an API span with the same name, as listed in [What Arize AX captures](#what-arize-ax-captures). That's expected. If a request has three spans of that name, another library also traces the Claude SDK, for example the OpenInference Anthropic instrumentor. Remove one of the two, or turn the SDK's spans off with `ANTHROPIC_OPEN_TELEMETRY=false`.
* **Spans have no prompts or responses.** Set the destination's content mode to **Full content** and `ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content` for the SDK. If your organization has a HIPAA, FERPA, or K-12 data-handling restriction with Anthropic, destinations export only metadata, whatever their content mode. The SDK's spans still carry content.

## Resources

<CardGroup>
  <Card horizontal icon="book-open" href="https://platform.claude.com/docs/en/manage-claude/opentelemetry" title="Export OpenTelemetry traces from the Claude API" />

  <Card horizontal icon="book-open" href="https://platform.claude.com/docs/en/managed-agents/overview" title="Claude Managed Agents documentation" />

  <Card horizontal icon="graduation-cap" href="/docs/ax/cookbooks/instrument/claude-managed-agents-guide" title="Trace and Evaluate Claude Managed Agents" />

  <Card horizontal icon="arrows-rotate" href="/docs/ax/concepts/otel-openinference/semantic-conventions" title="OpenInference and GenAI semantic conventions" />
</CardGroup>
